@astram/identity 0.11.0 → 0.12.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/cross-tenant-grant.object.d.ts.map +1 -1
- package/dist/cross-tenant-grant.object.js +3 -2
- package/dist/features/audit/directory-label-resolver.d.ts +4 -1
- package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
- package/dist/features/audit/directory-label-resolver.js +8 -5
- package/dist/features/audit/mariadb-audit-reader.d.ts +5 -1
- package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
- package/dist/features/audit/mariadb-audit-reader.js +10 -2
- package/dist/features/authz/access-bundle-reconcile.job.d.ts +4 -1
- package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/access-bundle-reconcile.job.js +7 -4
- package/dist/features/authz/access-bundle-store.d.ts +4 -1
- package/dist/features/authz/access-bundle-store.d.ts.map +1 -1
- package/dist/features/authz/access-bundle-store.js +19 -16
- package/dist/features/authz/catalog-reconcile.job.d.ts +3 -1
- package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/catalog-reconcile.job.js +3 -2
- package/dist/features/authz/db-permission-facts.adapter.d.ts +9 -3
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
- package/dist/features/authz/db-permission-facts.adapter.js +22 -16
- package/dist/features/authz/grant-admin.service.d.ts +4 -1
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +32 -37
- package/dist/features/authz/grant-ceiling.d.ts +6 -1
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +19 -16
- package/dist/features/authz/mariadb-catalog-store.d.ts +4 -1
- package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
- package/dist/features/authz/mariadb-catalog-store.js +10 -7
- package/dist/features/authz/permission-version.bumper.d.ts +6 -1
- package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
- package/dist/features/authz/permission-version.bumper.js +13 -11
- package/dist/features/authz/record-authorization.d.ts +8 -2
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +8 -4
- package/dist/features/authz/role-binding.d.ts +3 -2
- package/dist/features/authz/role-binding.d.ts.map +1 -1
- package/dist/features/authz/role-binding.js +7 -8
- package/dist/features/company/company-hierarchy.service.d.ts +6 -1
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
- package/dist/features/company/company-hierarchy.service.js +17 -16
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts +5 -1
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts.map +1 -1
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.js +18 -14
- package/dist/features/framework-schemas.token.d.ts +8 -0
- package/dist/features/framework-schemas.token.d.ts.map +1 -0
- package/dist/features/framework-schemas.token.js +10 -0
- package/dist/features/identity-feature.module.d.ts +29 -1
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +134 -33
- package/dist/features/membership/membership-checker.d.ts +7 -1
- package/dist/features/membership/membership-checker.d.ts.map +1 -1
- package/dist/features/membership/membership-checker.js +9 -4
- package/dist/features/migration-ledger-check.d.ts +15 -0
- package/dist/features/migration-ledger-check.d.ts.map +1 -0
- package/dist/features/migration-ledger-check.js +26 -0
- package/dist/features/scope/scope-closure.service.d.ts +8 -1
- package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
- package/dist/features/scope/scope-closure.service.js +22 -19
- package/dist/features/scope/scope-subtree.resolver.d.ts +7 -1
- package/dist/features/scope/scope-subtree.resolver.d.ts.map +1 -1
- package/dist/features/scope/scope-subtree.resolver.js +9 -4
- package/dist/features/service-account/lock-service-accounts.d.ts +3 -1
- package/dist/features/service-account/lock-service-accounts.d.ts.map +1 -1
- package/dist/features/service-account/lock-service-accounts.js +4 -4
- package/dist/features/service-account/no-expiry-key-report.d.ts +9 -3
- package/dist/features/service-account/no-expiry-key-report.d.ts.map +1 -1
- package/dist/features/service-account/no-expiry-key-report.js +17 -10
- package/dist/features/service-account/service-account.store.d.ts +8 -1
- package/dist/features/service-account/service-account.store.d.ts.map +1 -1
- package/dist/features/service-account/service-account.store.js +19 -14
- package/dist/features/user/user.service.d.ts +7 -3
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +12 -4
- package/dist/record-share.object.d.ts.map +1 -1
- package/dist/record-share.object.js +3 -2
- package/dist/schema.d.ts +15 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +17 -2
- package/migrations/1710000000001-CreateIdentityOrgModel.cjs +14 -4
- package/migrations/1710000000002-CreateMembershipAndPostings.cjs +14 -4
- package/migrations/1710000000003-CreateIdentityCredential.cjs +14 -4
- package/migrations/1710000000004-CreateSessionTables.cjs +14 -4
- package/migrations/1710000000005-CreateMfaAndTokenSurface.cjs +14 -4
- package/migrations/1710000000010-CreateAuditLog.cjs +14 -4
- package/migrations/1710000000011-CreateCrossTenantGrant.cjs +15 -4
- package/migrations/1710000000012-SeedCrossTenantGrantPerms.cjs +15 -4
- package/migrations/1710000000013-CreateSharedResource.cjs +14 -4
- package/migrations/1710000000018-CreateServiceAccount.cjs +13 -4
- package/migrations/1710000000019-AddSessionMustReenroll.cjs +14 -4
- package/migrations/1710000000020-CreateEmailChangeToken.cjs +15 -5
- package/migrations/1710000000021-AddUsersEmailUniqueIndex.cjs +24 -13
- package/migrations/1710000000022-CreateStoredFile.cjs +14 -4
- package/migrations/1710000000099-SeedDevIdentity.cjs +14 -6
- package/migrations/1710000000100-SeedDevTenantBAndRoot.cjs +14 -6
- package/package.json +4 -4
|
@@ -9,8 +9,11 @@ var IdentityFeatureModule_1;
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.IdentityFeatureModule = void 0;
|
|
11
11
|
exports.assertRequestPathUsesIdentityAuthorization = assertRequestPathUsesIdentityAuthorization;
|
|
12
|
+
exports.assertFrameworkSchemasAgree = assertFrameworkSchemasAgree;
|
|
13
|
+
exports.assertFrameworkDatabaseAgrees = assertFrameworkDatabaseAgrees;
|
|
12
14
|
const common_1 = require("@nestjs/common");
|
|
13
15
|
const core_1 = require("@nestjs/core");
|
|
16
|
+
const core_2 = require("@astram/core");
|
|
14
17
|
const engine_1 = require("@astram/engine");
|
|
15
18
|
const guard_1 = require("@astram/guard");
|
|
16
19
|
const objects_js_1 = require("../objects.js");
|
|
@@ -22,6 +25,7 @@ const role_service_js_1 = require("./role/role.service.js");
|
|
|
22
25
|
const role_controller_js_1 = require("./role/role.controller.js");
|
|
23
26
|
const user_service_js_1 = require("./user/user.service.js");
|
|
24
27
|
const email_address_changer_token_js_1 = require("./user/email-address-changer.token.js");
|
|
28
|
+
const framework_schemas_token_js_1 = require("./framework-schemas.token.js");
|
|
25
29
|
const user_controller_js_1 = require("./user/user.controller.js");
|
|
26
30
|
const membership_controller_js_1 = require("./membership/membership.controller.js");
|
|
27
31
|
const membership_service_js_1 = require("./membership/membership.service.js");
|
|
@@ -34,6 +38,7 @@ const permission_version_bumper_js_1 = require("./authz/permission-version.bumpe
|
|
|
34
38
|
const record_authorization_js_1 = require("./authz/record-authorization.js");
|
|
35
39
|
const db_permission_facts_adapter_js_1 = require("./authz/db-permission-facts.adapter.js");
|
|
36
40
|
const session_revocation_check_js_1 = require("./user/session-revocation-check.js");
|
|
41
|
+
const migration_ledger_check_js_1 = require("./migration-ledger-check.js");
|
|
37
42
|
const access_bundle_store_js_1 = require("./authz/access-bundle-store.js");
|
|
38
43
|
const grant_admin_service_js_1 = require("./authz/grant-admin.service.js");
|
|
39
44
|
const grant_admin_controller_js_1 = require("./authz/grant-admin.controller.js");
|
|
@@ -91,9 +96,59 @@ function assertRequestPathUsesIdentityAuthorization(authority) {
|
|
|
91
96
|
}
|
|
92
97
|
return true;
|
|
93
98
|
}
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
/** DI token of the boot check that identity and guard use the same framework database names. */
|
|
100
|
+
const IDENTITY_FRAMEWORK_SCHEMAS_CHECK = Symbol('IDENTITY_FRAMEWORK_SCHEMAS_CHECK');
|
|
101
|
+
/**
|
|
102
|
+
* The boot check behind {@link IDENTITY_FRAMEWORK_SCHEMAS_CHECK} (configurable-framework-db,
|
|
103
|
+
* ADR-0037; reviewer R2): the names `IdentityFeatureModule.forRoot` resolved — and handed to the
|
|
104
|
+
* engine, the audit sinks, the reader and every identity store — must equal guard's
|
|
105
|
+
* `FRAMEWORK_SCHEMAS`, or engine/guard/identity would split across databases. A guard that
|
|
106
|
+
* provides no `FRAMEWORK_SCHEMAS` (older than this seam) may only run on the default names.
|
|
107
|
+
* Throws (fail closed) with the namespaces and names only.
|
|
108
|
+
*/
|
|
109
|
+
function assertFrameworkSchemasAgree(identity, guard) {
|
|
110
|
+
const names = (s) => `namespace=${s.namespace} app=${s.app} identity=${s.identity} authz=${s.authz} audit=${s.audit}`;
|
|
111
|
+
if (guard === undefined || guard === null) {
|
|
112
|
+
if (identity.isDefault)
|
|
113
|
+
return true;
|
|
114
|
+
throw new Error(`IdentityFeatureModule: resolved ${names(identity)}, but @astram/guard provides no ` +
|
|
115
|
+
'FRAMEWORK_SCHEMAS; upgrade @astram/guard so every framework module uses the same names.');
|
|
116
|
+
}
|
|
117
|
+
const same = identity.app === guard.app &&
|
|
118
|
+
identity.identity === guard.identity &&
|
|
119
|
+
identity.authz === guard.authz &&
|
|
120
|
+
identity.audit === guard.audit;
|
|
121
|
+
if (!same) {
|
|
122
|
+
throw new Error(`IdentityFeatureModule: framework database names disagree — identity ${names(identity)}; ` +
|
|
123
|
+
`guard ${names(guard)}. Pass the same names (or none) to ShieldModule and ` +
|
|
124
|
+
'IdentityFeatureModule.');
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
/** DI token of the boot check that guard's runtime database equals identity's (QA P2-5). */
|
|
129
|
+
const IDENTITY_FRAMEWORK_DATABASE_CHECK = Symbol('IDENTITY_FRAMEWORK_DATABASE_CHECK');
|
|
130
|
+
/**
|
|
131
|
+
* The boot check behind {@link IDENTITY_FRAMEWORK_DATABASE_CHECK} (G-007 QA P2-5): the runtime
|
|
132
|
+
* database identity's engine and authz pool connect to (derived from `forRoot({ schemas })`)
|
|
133
|
+
* must equal guard's `APP_CONFIG.db.database`, or unqualified queries of guard and identity would
|
|
134
|
+
* land in different databases. No guard `APP_CONFIG` visible ⇒ nothing to compare. Throws (fail
|
|
135
|
+
* closed) with database names only.
|
|
136
|
+
*/
|
|
137
|
+
function assertFrameworkDatabaseAgrees(identityDatabase, guardConfig) {
|
|
138
|
+
const guardDatabase = guardConfig?.db?.database;
|
|
139
|
+
if (guardDatabase === undefined || guardDatabase === identityDatabase)
|
|
140
|
+
return true;
|
|
141
|
+
throw new Error(`IdentityFeatureModule: runtime database ${identityDatabase} differs from @astram/guard's ` +
|
|
142
|
+
`${guardDatabase}. Pass the same names (or none) to ShieldModule and ` +
|
|
143
|
+
'IdentityFeatureModule, and one APP_DB_NAME.');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Map guard's DbConfig → the engine's EngineDbConfig (adds the pool cap the engine needs). The
|
|
147
|
+
* database and default user follow `schemas` (the names `forRoot` resolved or was given); an
|
|
148
|
+
* explicit `APP_DB_USER` / `APP_DB_NAME` still wins (QA P2-5).
|
|
149
|
+
*/
|
|
150
|
+
function engineDbFromEnv(schemas) {
|
|
151
|
+
const db = (0, guard_1.loadDbConfig)(process.env, schemas);
|
|
97
152
|
const ssl = (0, guard_1.mariadbSslOption)(db.tls);
|
|
98
153
|
return {
|
|
99
154
|
host: db.host,
|
|
@@ -134,22 +189,34 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
134
189
|
* cross-tenant / policy authority, grant ceiling, guided rule-builder schema and owner-share
|
|
135
190
|
* resolver. Without them a consumer type is unregistered everywhere (denied), not policy-
|
|
136
191
|
* authorable and not shareable ("type '…' is not registered — cannot share").
|
|
192
|
+
* @param schemas the resolved framework database names (configurable-framework-db, ADR-0037).
|
|
193
|
+
* Absent ⇒ resolved ONCE here from `SHIELD_DB_NAMESPACE` (unset ⇒ `shield_*`; invalid ⇒
|
|
194
|
+
* throws). The one set is handed to the engine (registrations + its audit sink), the seven
|
|
195
|
+
* audit sinks, the audit reader and every identity store; a boot check refuses names that
|
|
196
|
+
* differ from guard's `FRAMEWORK_SCHEMAS`. Never read per request.
|
|
137
197
|
*/
|
|
138
|
-
static forRoot({ rootIds, extraObjects = [], } = {}) {
|
|
198
|
+
static forRoot({ rootIds, extraObjects = [], schemas: givenSchemas, } = {}) {
|
|
199
|
+
// The framework database names, resolved ONCE (boot) for every SQL path built below.
|
|
200
|
+
// Caller-supplied names are checked before any SQL is built (fail closed, Shield #1).
|
|
201
|
+
const schemas = givenSchemas !== undefined
|
|
202
|
+
? (0, core_2.assertFrameworkSchemaNames)(givenSchemas, 'IdentityFeatureModule.forRoot schemas')
|
|
203
|
+
: (0, core_2.resolveFrameworkSchemas)(process.env);
|
|
139
204
|
// ONE shared shield_authz pool for the whole grant-admin stack (ADR-0011): the
|
|
140
205
|
// permission-version bumper, the AccessBundleStore, the GrantAdminService, and the two
|
|
141
206
|
// reconcile jobs all run over this single pool (don't open several). Opened once at
|
|
142
207
|
// forRoot() (not import time) — mirrors the pre-existing bump-pool pattern. Since Phase 9 #7
|
|
143
208
|
// it also backs the request path's decision authority (provided to ShieldModule below).
|
|
144
|
-
const authzPool = (0, record_authorization_js_1.createIdentityAuthzPool)();
|
|
209
|
+
const authzPool = (0, record_authorization_js_1.createIdentityAuthzPool)(schemas);
|
|
210
|
+
// QA P2-5: the engine and the authz pool connect to the database derived from `schemas`.
|
|
211
|
+
const engineDb = engineDbFromEnv(schemas);
|
|
145
212
|
// The ONE record authorization (Phase 9 #7, closes KI #55/#57): built here from this module's
|
|
146
213
|
// own `extraObjects`, provided to ShieldModule under SHIELD_RECORD_AUTHORIZATION, and reused for
|
|
147
214
|
// every internal authority below. Its registry is the ONE key universe: the grant ceiling sees
|
|
148
215
|
// every key the request path grants (review M5 / Phase 8 #4), and a consumer type is
|
|
149
216
|
// registered for the cross-tenant writer and the policy author too.
|
|
150
|
-
const recordAuthorization = (0, record_authorization_js_1.buildRecordAuthorizationInputs)(authzPool, extraObjects);
|
|
217
|
+
const recordAuthorization = (0, record_authorization_js_1.buildRecordAuthorizationInputs)(authzPool, extraObjects, schemas);
|
|
151
218
|
const ceilingRegistry = recordAuthorization.registry;
|
|
152
|
-
const bumper = new permission_version_bumper_js_1.PermissionVersionBumper();
|
|
219
|
+
const bumper = new permission_version_bumper_js_1.PermissionVersionBumper(undefined, schemas);
|
|
153
220
|
const postingVersionBump = {
|
|
154
221
|
bumpTenant: (tenantUuid) => bumper.bumpForTenant(tenantUuid, authzPool),
|
|
155
222
|
// A user disabled/re-enabled: every membership tenant (autocommit pool → published at once).
|
|
@@ -162,7 +229,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
162
229
|
// self-escalation ceiling: a non-root grantor may only grant a key they themselves hold —
|
|
163
230
|
// resolved over the SAME authz pool + registry the object-type gate uses (one source of
|
|
164
231
|
// truth for "what a principal holds"). ALWAYS wired here, so production always enforces.
|
|
165
|
-
const accessBundleStore = new access_bundle_store_js_1.AccessBundleStore(authzPool);
|
|
232
|
+
const accessBundleStore = new access_bundle_store_js_1.AccessBundleStore(authzPool, schemas);
|
|
166
233
|
// (`ceilingRegistry` is the shared record-authorization registry built at the top of forRoot.)
|
|
167
234
|
// `rootIds` marks platform roots as never dominated.
|
|
168
235
|
// rootIds is passed EXPLICITLY (review F5): `null` states "not configured". The dominance
|
|
@@ -173,30 +240,33 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
173
240
|
'ShieldModule.forRoot({ rootIds }). Platform administrators are still protected via ' +
|
|
174
241
|
'users.u_is_platform_superuser.');
|
|
175
242
|
}
|
|
176
|
-
const grantCeiling = new grant_ceiling_js_1.GrantCeiling(authzPool, ceilingRegistry, rootIds ?? null);
|
|
243
|
+
const grantCeiling = new grant_ceiling_js_1.GrantCeiling(authzPool, ceilingRegistry, rootIds ?? null, undefined, schemas);
|
|
244
|
+
// Every identity audit sink writes `<audit>.audit_log` of the resolved names (Shield #6: the
|
|
245
|
+
// audit DB of the same namespace).
|
|
246
|
+
const auditSinkConfig = { schema: schemas.audit };
|
|
177
247
|
// Grant administration is audited on the grant's own transaction (D6, fail-closed).
|
|
178
|
-
const grantAdminAuditSink = new engine_1.MariaDbAuditSink(
|
|
179
|
-
const grantAdminService = new grant_admin_service_js_1.GrantAdminService(authzPool, bumper, accessBundleStore, grantCeiling, grantAdminAuditSink);
|
|
248
|
+
const grantAdminAuditSink = new engine_1.MariaDbAuditSink(auditSinkConfig);
|
|
249
|
+
const grantAdminService = new grant_admin_service_js_1.GrantAdminService(authzPool, bumper, accessBundleStore, grantCeiling, grantAdminAuditSink, schemas);
|
|
180
250
|
// Scope closure (Task 1.2 of the scoped-hierarchy program): the reconciler + node
|
|
181
251
|
// move/delete maintenance, over the SAME shared authzPool (it reaches BOTH shield_app —
|
|
182
252
|
// companies — and shield_authz — scope_closure). Reuses the SAME `bumper` instance so a
|
|
183
253
|
// moveNode's session-invalidation joins the move's transaction. reconcileClosure() is
|
|
184
254
|
// kicked post-boot by the CatalogBootstrapProvider (mirrors the catalog reconcile).
|
|
185
|
-
const scopeClosureService = new scope_closure_service_js_1.ScopeClosureService(authzPool, bumper);
|
|
255
|
+
const scopeClosureService = new scope_closure_service_js_1.ScopeClosureService(authzPool, bumper, schemas);
|
|
186
256
|
// Company hierarchy (security hardening #2): the ONLY path that moves/deletes/attaches a company
|
|
187
257
|
// node — through the closure service above — audited on the move/delete transaction.
|
|
188
258
|
const companyHierarchyService = new company_hierarchy_service_js_1.CompanyHierarchyService(authzPool, scopeClosureService, grantCeiling, // Phase 8 #3: a company delete must dominate every member
|
|
189
|
-
new engine_1.MariaDbAuditSink(
|
|
259
|
+
new engine_1.MariaDbAuditSink(auditSinkConfig), undefined, schemas);
|
|
190
260
|
// Audit viewer (Phase 5): the AuditReader + LabelResolver over the SAME shared authzPool
|
|
191
261
|
// (the reader queries shield_audit + the resolver/actor-lookup query shield_app, both
|
|
192
262
|
// reachable by that pool). `auditNameToId` is the actor-name -> uuid reverse lookup for the
|
|
193
263
|
// actor filter (astram3's exact closure). A mariadb Pool.query(sql, params) structurally
|
|
194
264
|
// satisfies the reader/resolver's `AuditQueryable` — pass authzPool directly. Built at
|
|
195
265
|
// forRoot() (not via Nest DI) because it needs the hand-built pool, then provided useValue.
|
|
196
|
-
const auditReader = new mariadb_audit_reader_js_1.MariaDbAuditReader(authzPool);
|
|
197
|
-
const auditLabels = new directory_label_resolver_js_1.DirectoryLabelResolver(authzPool);
|
|
266
|
+
const auditReader = new mariadb_audit_reader_js_1.MariaDbAuditReader(authzPool, { schema: schemas.audit });
|
|
267
|
+
const auditLabels = new directory_label_resolver_js_1.DirectoryLabelResolver(authzPool, schemas);
|
|
198
268
|
const auditNameToId = async (name) => {
|
|
199
|
-
const rows = (await authzPool.query(
|
|
269
|
+
const rows = (await authzPool.query(`SELECT u_uuid AS id FROM \`${schemas.app}\`.\`users\` WHERE u_username = ? LIMIT 1`, [name]));
|
|
200
270
|
return rows[0]?.id ?? null;
|
|
201
271
|
};
|
|
202
272
|
const auditReadService = new audit_read_service_js_1.AuditReadService(auditReader, auditLabels, auditNameToId);
|
|
@@ -220,7 +290,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
220
290
|
// consumer passes to ShieldModule.forRoot (root-correctness for control-plane grants).
|
|
221
291
|
const authorizeCrossTenantGrant = (0, grant_authorize_js_1.makeAuthorizeCrossTenantGrant)({
|
|
222
292
|
authority: crossTenantAuthority,
|
|
223
|
-
readRoleIds: (principalId, tenantId) => (0, db_permission_facts_adapter_js_1.readRoleIdsForTenant)(authzPool, principalId, tenantId),
|
|
293
|
+
readRoleIds: (principalId, tenantId) => (0, db_permission_facts_adapter_js_1.readRoleIdsForTenant)(authzPool, principalId, tenantId, schemas),
|
|
224
294
|
isRoot: (principalId) => (rootIds ?? []).includes(principalId),
|
|
225
295
|
});
|
|
226
296
|
// Audit bridge: map guard's `GrantAuditEvent` → the engine's `MariaDbAuditSink` (the SAME
|
|
@@ -235,7 +305,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
235
305
|
// never happen for a cross-tenant grant) is de-duped to ONE row.
|
|
236
306
|
// BEST-EFFORT: a throw here must NEVER fail the lifecycle write (the writer already swallows a
|
|
237
307
|
// sink throw; we also guard here so a mapping/DB error is inert).
|
|
238
|
-
const grantAuditSink = new engine_1.MariaDbAuditSink(
|
|
308
|
+
const grantAuditSink = new engine_1.MariaDbAuditSink(auditSinkConfig);
|
|
239
309
|
const grantAudit = {
|
|
240
310
|
record: async (event) => {
|
|
241
311
|
try {
|
|
@@ -266,6 +336,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
266
336
|
db: authzPool,
|
|
267
337
|
authorize: authorizeCrossTenantGrant,
|
|
268
338
|
audit: grantAudit,
|
|
339
|
+
schema: schemas.authz,
|
|
269
340
|
});
|
|
270
341
|
// The read side (inbox/mine/incoming/offerable-types) over the same pool + the merged object
|
|
271
342
|
// set (offerable = tenantScoped types only, the consumer's included, KI #57) + the per-type
|
|
@@ -278,7 +349,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
278
349
|
return [reg.typeKey, (0, engine_1.effectiveActions)(reg)];
|
|
279
350
|
}));
|
|
280
351
|
const actionsByType = (typeKey) => actionsByTypeKey.get(typeKey);
|
|
281
|
-
const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects, ...extraObjects], (typeKey) => actionsByType(typeKey) ?? []);
|
|
352
|
+
const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects, ...extraObjects], (typeKey) => actionsByType(typeKey) ?? [], schemas);
|
|
282
353
|
// ── GUIDED RULE-BUILDER (list-scoping engine Phase 5, §9.1) ──────────────────────────────
|
|
283
354
|
// The policy-authoring surface over the guard `MariaDbPolicyStore` + core `makeAuthorPolicy`.
|
|
284
355
|
// Reuses the SAME shared registry + `crossTenantAuthority` (a DecisionAuthority over the
|
|
@@ -286,7 +357,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
286
357
|
// core AuditSink is one-arg (`record(event)`); the engine `MariaDbAuditSink` is two-arg
|
|
287
358
|
// (`record(event, query)`) — so bind the query to the shared authzPool (mirrors the
|
|
288
359
|
// `grantAudit` bridge above). Best-effort audit (a sink throw must not fail the author).
|
|
289
|
-
const policyAuditSink = new engine_1.MariaDbAuditSink(
|
|
360
|
+
const policyAuditSink = new engine_1.MariaDbAuditSink(auditSinkConfig);
|
|
290
361
|
const policyAudit = {
|
|
291
362
|
record: async (event) => {
|
|
292
363
|
try {
|
|
@@ -297,7 +368,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
297
368
|
}
|
|
298
369
|
},
|
|
299
370
|
};
|
|
300
|
-
const policyStore = new guard_1.MariaDbPolicyStore(authzPool);
|
|
371
|
+
const policyStore = new guard_1.MariaDbPolicyStore(authzPool, { schema: schemas.authz });
|
|
301
372
|
// The guided-builder SCHEMA source — the authorable field/action metadata, reflected from the
|
|
302
373
|
// registered identity object types (engine `reflectObjectType` yields fields WITH their
|
|
303
374
|
// `type`, which the core registry descriptor drops). Only tenantScoped, owned DATA-plane types
|
|
@@ -335,7 +406,9 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
335
406
|
// and the writer rejects "type 'note' is not registered — cannot share." (Live-acceptance fix.)
|
|
336
407
|
const shareTypeInfoByKey = new Map();
|
|
337
408
|
for (const input of [...objects_js_1.identityObjects, ...extraObjects]) {
|
|
338
|
-
|
|
409
|
+
// R1 / KI #1 (spec v2 §5.B): resolve `FRAMEWORK_SCHEMA` tokens to the resolved names before
|
|
410
|
+
// the schema reaches `RecordShareWriter` SQL; an unresolvable token throws (fail closed).
|
|
411
|
+
const reg = (0, engine_1.resolveObjectTypeSchema)(typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input, schemas);
|
|
339
412
|
const owner = (0, engine_1.ownerColumn)(reg);
|
|
340
413
|
const pk = reg.primaryKey.kind === 'uuid' ? (0, engine_1.uuidPkColumn)(reg.primaryKey) : null;
|
|
341
414
|
// Only OWNED types with a uuid PK + an owner column are shareable; others are omitted (the
|
|
@@ -355,10 +428,11 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
355
428
|
const shareTypeResolver = {
|
|
356
429
|
resolve: (typeKey) => shareTypeInfoByKey.get(typeKey),
|
|
357
430
|
};
|
|
358
|
-
const shareAuditSink = new engine_1.MariaDbAuditSink(
|
|
431
|
+
const shareAuditSink = new engine_1.MariaDbAuditSink(auditSinkConfig);
|
|
359
432
|
const recordShareWriter = new guard_1.RecordShareWriter({
|
|
360
433
|
db: authzPool,
|
|
361
434
|
types: shareTypeResolver,
|
|
435
|
+
schema: schemas.authz,
|
|
362
436
|
audit: {
|
|
363
437
|
record: async (event) => {
|
|
364
438
|
try {
|
|
@@ -386,11 +460,11 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
386
460
|
// shield_app.service_account — the accounts — AND shield_authz.service_account_key — the hashed
|
|
387
461
|
// keys). The guard's MariaDbApiKeyStore + Argon2idPasswordHasher back the credential lifecycle;
|
|
388
462
|
// the service enforces own-tenant + the ADR-0029 ceiling (scope ⊆ mine, roles ⊆ mine).
|
|
389
|
-
const serviceAccountStore = new service_account_store_js_1.ServiceAccountStore(authzPool);
|
|
390
|
-
const serviceAccountKeyStore = new guard_1.MariaDbApiKeyStore(authzPool);
|
|
463
|
+
const serviceAccountStore = new service_account_store_js_1.ServiceAccountStore(authzPool, schemas);
|
|
464
|
+
const serviceAccountKeyStore = new guard_1.MariaDbApiKeyStore(authzPool, schemas);
|
|
391
465
|
const serviceAccountHasher = new guard_1.Argon2idPasswordHasher();
|
|
392
466
|
// Best-effort audit → the SAME shield_audit sink pattern (a sink throw never fails the op).
|
|
393
|
-
const serviceAccountAuditSink = new engine_1.MariaDbAuditSink(
|
|
467
|
+
const serviceAccountAuditSink = new engine_1.MariaDbAuditSink(auditSinkConfig);
|
|
394
468
|
const serviceAccountAudit = {
|
|
395
469
|
record: async (event) => {
|
|
396
470
|
try {
|
|
@@ -412,12 +486,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
412
486
|
// shield_app.users lookup the audit actor filter uses (inverted). Unknown → null (view falls
|
|
413
487
|
// back to no name, never an id).
|
|
414
488
|
const resolveActorName = async (id) => {
|
|
415
|
-
const rows = (await authzPool.query(
|
|
489
|
+
const rows = (await authzPool.query(`SELECT u_username AS name FROM \`${schemas.app}\`.\`users\` WHERE u_uuid = ? LIMIT 1`, [id]));
|
|
416
490
|
return rows[0]?.name ?? null;
|
|
417
491
|
};
|
|
418
492
|
const serviceAccountService = new service_account_service_js_1.ServiceAccountService(serviceAccountStore, serviceAccountKeyStore, serviceAccountHasher, serviceAccountAudit, resolveActorName,
|
|
419
493
|
// The ceiling's roles: in-effect postings (the same source as gate 5 and GrantCeiling).
|
|
420
|
-
(principalId, tenantId) => (0, db_permission_facts_adapter_js_1.readRoleIdsForTenant)(authzPool, principalId, tenantId));
|
|
494
|
+
(principalId, tenantId) => (0, db_permission_facts_adapter_js_1.readRoleIdsForTenant)(authzPool, principalId, tenantId, schemas));
|
|
421
495
|
// NOTE: the machine AUTH path (the `ServiceAuthenticator` behind `SHIELD_SERVICE_AUTHENTICATOR`)
|
|
422
496
|
// is provided by the guard's own AuthModule (under `auth: true`), NOT here — a token provided in
|
|
423
497
|
// this later, sibling feature module is invisible to the APP_GUARD in ShieldModule (that DI
|
|
@@ -427,7 +501,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
427
501
|
// Stored files (file-storage SPEC §5 I-K): `file_access` events go to the SAME shield_audit
|
|
428
502
|
// sink over the shared authzPool. Not best-effort here: FilesService decides (a read event
|
|
429
503
|
// that cannot be written withholds the bytes).
|
|
430
|
-
const filesAuditSink = new engine_1.MariaDbAuditSink(
|
|
504
|
+
const filesAuditSink = new engine_1.MariaDbAuditSink(auditSinkConfig);
|
|
431
505
|
const filesAudit = {
|
|
432
506
|
record: (event) => filesAuditSink.record(event, (sql, params) => authzPool.query(sql, params)),
|
|
433
507
|
};
|
|
@@ -442,10 +516,13 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
442
516
|
// snapshot is empty and no bundles reconcile into shield_authz.access_bundle.
|
|
443
517
|
engine_1.EngineModule.forFeature([], { accessBundles: index_js_1.identityAccessBundles }),
|
|
444
518
|
// The engine's runtime CRUD pipe + the identity object registrations.
|
|
519
|
+
// R2: the SAME resolved names, so the engine's registrations (FRAMEWORK_SCHEMA tokens) and
|
|
520
|
+
// its audit sink cannot land in another namespace than identity's own SQL.
|
|
445
521
|
engine_1.EngineModule.forRoot({
|
|
446
|
-
db:
|
|
522
|
+
db: engineDb,
|
|
447
523
|
registrations: [...objects_js_1.identityObjects],
|
|
448
524
|
listScope,
|
|
525
|
+
schemas,
|
|
449
526
|
}),
|
|
450
527
|
// NestJS DiscoveryService/MetadataScanner — the boot bootstrap provider reflects the
|
|
451
528
|
// live controllers through this to build the OperationInventory.
|
|
@@ -499,8 +576,11 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
499
576
|
// (collision refusal + login-hint follow in one transaction), over the shared authz pool.
|
|
500
577
|
{
|
|
501
578
|
provide: email_address_changer_token_js_1.IDENTITY_EMAIL_ADDRESS_CHANGER,
|
|
502
|
-
useValue: new guard_1.MariaDbEmailAddressChanger(authzPool),
|
|
579
|
+
useValue: new guard_1.MariaDbEmailAddressChanger(authzPool, Date.now, schemas),
|
|
503
580
|
},
|
|
581
|
+
// The same resolved names for identity services Nest constructs (UserService's
|
|
582
|
+
// email-change audit sink), never a second env read.
|
|
583
|
+
{ provide: framework_schemas_token_js_1.IDENTITY_FRAMEWORK_SCHEMAS, useValue: schemas },
|
|
504
584
|
// ADR-0035 (GAPS #6): attach the guard's Redis permission-version cache to the ONE shared
|
|
505
585
|
// bumper, so every committed bump (postings, grants, hierarchy) is published to Redis,
|
|
506
586
|
// where gate 5 reads it. Provided by the auth surface under `auth: true` (through the
|
|
@@ -522,6 +602,27 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
522
602
|
useFactory: assertRequestPathUsesIdentityAuthorization,
|
|
523
603
|
inject: [{ token: guard_1.SHIELD_DECISION_AUTHORITY, optional: true }],
|
|
524
604
|
},
|
|
605
|
+
// configurable-framework-db (R2): fail the boot unless guard's FRAMEWORK_SCHEMAS equal the
|
|
606
|
+
// names this module resolved (and gave the engine, the sinks and every store).
|
|
607
|
+
{
|
|
608
|
+
provide: IDENTITY_FRAMEWORK_SCHEMAS_CHECK,
|
|
609
|
+
useFactory: (guardSchemas) => assertFrameworkSchemasAgree(schemas, guardSchemas),
|
|
610
|
+
inject: [{ token: guard_1.FRAMEWORK_SCHEMAS, optional: true }],
|
|
611
|
+
},
|
|
612
|
+
// QA P2-5: and guard's runtime database must be the one identity connects to.
|
|
613
|
+
{
|
|
614
|
+
provide: IDENTITY_FRAMEWORK_DATABASE_CHECK,
|
|
615
|
+
useFactory: (guardConfig) => assertFrameworkDatabaseAgrees(engineDb.database, guardConfig),
|
|
616
|
+
inject: [{ token: guard_1.APP_CONFIG, optional: true }],
|
|
617
|
+
},
|
|
618
|
+
// configurable-framework-db spec 5.D: the identity migrations are in the ledger, checked
|
|
619
|
+
// once at application bootstrap over the shared runtime pool (production fails, dev warns).
|
|
620
|
+
// Guard + identity ids; one ledger read shared with AuthModule's check (guard's registry).
|
|
621
|
+
{
|
|
622
|
+
provide: migration_ledger_check_js_1.IDENTITY_MIGRATION_LEDGER_CHECK,
|
|
623
|
+
useFactory: (registry) => (0, migration_ledger_check_js_1.identityMigrationLedgerCheck)(authzPool, schemas, process.env, registry ?? null),
|
|
624
|
+
inject: [{ token: guard_1.SHIELD_MIGRATION_LEDGER_REGISTRY, optional: true }],
|
|
625
|
+
},
|
|
525
626
|
{
|
|
526
627
|
provide: IDENTITY_VERSION_PUBLISHER_LINK,
|
|
527
628
|
useFactory: (cache) => {
|
|
@@ -562,13 +663,13 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
562
663
|
// ceiling know, so a consumer type's keys are grantable and its rows are not orphaned.
|
|
563
664
|
{
|
|
564
665
|
provide: catalog_reconcile_job_js_1.CatalogReconcileJob,
|
|
565
|
-
useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, recordAuthorization.registry),
|
|
666
|
+
useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, recordAuthorization.registry, schemas),
|
|
566
667
|
},
|
|
567
668
|
// The bundle half: reconciles the merged AccessBundleSpec[] (ENGINE_ALL_ACCESS_BUNDLES,
|
|
568
669
|
// reflected by the engine from identityAccessBundles) into shield_authz.access_bundle.
|
|
569
670
|
{
|
|
570
671
|
provide: access_bundle_reconcile_job_js_1.AccessBundleReconcileJob,
|
|
571
|
-
useFactory: (bundles) => new access_bundle_reconcile_job_js_1.AccessBundleReconcileJob(authzPool, bundles),
|
|
672
|
+
useFactory: (bundles) => new access_bundle_reconcile_job_js_1.AccessBundleReconcileJob(authzPool, bundles, schemas),
|
|
572
673
|
inject: [engine_1.ENGINE_ALL_ACCESS_BUNDLES],
|
|
573
674
|
},
|
|
574
675
|
// Scope closure reconciler + node move/delete maintenance (Task 1.2), built at forRoot()
|
|
@@ -30,9 +30,15 @@
|
|
|
30
30
|
* a switch. All SQL values are `?`-bound (never interpolated).
|
|
31
31
|
*/
|
|
32
32
|
import type { Pool } from 'mariadb';
|
|
33
|
+
import { type IdentitySchemaNames } from '../../schema.js';
|
|
33
34
|
export declare class DbMembershipChecker {
|
|
34
35
|
private readonly pool;
|
|
35
|
-
|
|
36
|
+
private readonly schemas;
|
|
37
|
+
/**
|
|
38
|
+
* @param schemas resolved framework names; the membership table lives in `schemas.app`.
|
|
39
|
+
* Absent ⇒ resolved from the namespace, once, here.
|
|
40
|
+
*/
|
|
41
|
+
constructor(pool: Pool, schemas?: IdentitySchemaNames);
|
|
36
42
|
/**
|
|
37
43
|
* Does `principalId` hold an ACTIVE membership at `nodeId`? A single existence read —
|
|
38
44
|
* `true` iff a row with `cm_is_active = 1 AND cm_status = 'active'` exists. Throws on a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership-checker.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership-checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"membership-checker.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership-checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEpF,qBAAa,mBAAmB;IAM5B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B;;;OAGG;gBAEgB,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,mBAA+C;IAG3E;;;;OAIG;IACG,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAWjF"}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DbMembershipChecker = void 0;
|
|
4
|
-
|
|
5
|
-
const APP = 'shield_app';
|
|
4
|
+
const schema_js_1 = require("../../schema.js");
|
|
6
5
|
class DbMembershipChecker {
|
|
7
6
|
pool;
|
|
8
|
-
|
|
7
|
+
schemas;
|
|
8
|
+
/**
|
|
9
|
+
* @param schemas resolved framework names; the membership table lives in `schemas.app`.
|
|
10
|
+
* Absent ⇒ resolved from the namespace, once, here.
|
|
11
|
+
*/
|
|
12
|
+
constructor(pool, schemas = (0, schema_js_1.frameworkSchemasFromEnv)()) {
|
|
9
13
|
this.pool = pool;
|
|
14
|
+
this.schemas = schemas;
|
|
10
15
|
}
|
|
11
16
|
/**
|
|
12
17
|
* Does `principalId` hold an ACTIVE membership at `nodeId`? A single existence read —
|
|
@@ -15,7 +20,7 @@ class DbMembershipChecker {
|
|
|
15
20
|
*/
|
|
16
21
|
async hasActiveMembership(principalId, nodeId) {
|
|
17
22
|
const rows = (await this.pool.query(`SELECT 1 AS ok
|
|
18
|
-
FROM \`${
|
|
23
|
+
FROM \`${this.schemas.app}\`.\`company_membership\`
|
|
19
24
|
WHERE cm_user_uuid = ? AND cm_company_uuid = ?
|
|
20
25
|
AND cm_is_active = 1 AND cm_status = 'active'
|
|
21
26
|
LIMIT 1`, [principalId, nodeId]));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FrameworkSchemas } from '@astram/core';
|
|
2
|
+
import { FrameworkMigrationLedgerCheck, type MigrationLedgerRegistry } from '@astram/guard';
|
|
3
|
+
/** DI token of identity's ledger check (guard's `AuthModule` uses its own token). */
|
|
4
|
+
export declare const IDENTITY_MIGRATION_LEDGER_CHECK: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* configurable-framework-db spec 5.D: the boot hook that checks every guard AND identity
|
|
7
|
+
* migration is in the `@astram/migrate` ledger (identity reads guard's authz tables; guard never
|
|
8
|
+
* imports identity, ADR-0003). Runs once at application bootstrap, never per request (Shield
|
|
9
|
+
* #12). With guard's per-app `registry`, it shares one ledger read with `AuthModule`'s check.
|
|
10
|
+
* Outside `NODE_ENV` development/test a missing id, ledger table or unreadable ledger fails boot.
|
|
11
|
+
*/
|
|
12
|
+
export declare function identityMigrationLedgerCheck(pool: {
|
|
13
|
+
query(sql: string): Promise<unknown>;
|
|
14
|
+
}, schemas: FrameworkSchemas, env?: NodeJS.ProcessEnv, registry?: MigrationLedgerRegistry | null): FrameworkMigrationLedgerCheck;
|
|
15
|
+
//# sourceMappingURL=migration-ledger-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-ledger-check.d.ts","sourceRoot":"","sources":["../../src/features/migration-ledger-check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EACL,6BAA6B,EAG7B,KAAK,uBAAuB,EAC7B,MAAM,eAAe,CAAC;AAGvB,qFAAqF;AACrF,eAAO,MAAM,+BAA+B,eAA4C,CAAC;AAEzF;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE;IAAE,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAC9C,OAAO,EAAE,gBAAgB,EACzB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,QAAQ,GAAE,uBAAuB,GAAG,IAAW,GAC9C,6BAA6B,CAY/B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IDENTITY_MIGRATION_LEDGER_CHECK = void 0;
|
|
4
|
+
exports.identityMigrationLedgerCheck = identityMigrationLedgerCheck;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const guard_1 = require("@astram/guard");
|
|
7
|
+
const migrations_dir_js_1 = require("../migrations-dir.js");
|
|
8
|
+
/** DI token of identity's ledger check (guard's `AuthModule` uses its own token). */
|
|
9
|
+
exports.IDENTITY_MIGRATION_LEDGER_CHECK = Symbol('IDENTITY_MIGRATION_LEDGER_CHECK');
|
|
10
|
+
/**
|
|
11
|
+
* configurable-framework-db spec 5.D: the boot hook that checks every guard AND identity
|
|
12
|
+
* migration is in the `@astram/migrate` ledger (identity reads guard's authz tables; guard never
|
|
13
|
+
* imports identity, ADR-0003). Runs once at application bootstrap, never per request (Shield
|
|
14
|
+
* #12). With guard's per-app `registry`, it shares one ledger read with `AuthModule`'s check.
|
|
15
|
+
* Outside `NODE_ENV` development/test a missing id, ledger table or unreadable ledger fails boot.
|
|
16
|
+
*/
|
|
17
|
+
function identityMigrationLedgerCheck(pool, schemas, env = process.env, registry = null) {
|
|
18
|
+
return new guard_1.FrameworkMigrationLedgerCheck({
|
|
19
|
+
query: (sql) => pool.query(sql),
|
|
20
|
+
// Same candidates as guard's check (G-007 QA P1), so the registry shares one probe + read.
|
|
21
|
+
...(0, guard_1.resolveMigrationLedgerCheck)(env, schemas),
|
|
22
|
+
migrationsDir: [guard_1.guardMigrationsDir, migrations_dir_js_1.identityMigrationsDir],
|
|
23
|
+
nodeEnv: env.NODE_ENV,
|
|
24
|
+
logger: new common_1.Logger('IdentityFeatureModule'),
|
|
25
|
+
}, registry);
|
|
26
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ScopeEdge } from '@astram/guard';
|
|
2
|
+
import { type IdentitySchemaNames } from '../../schema.js';
|
|
2
3
|
/**
|
|
3
4
|
* The minimal query surface a transaction connection must expose. A `mariadb`
|
|
4
5
|
* `PoolConnection` satisfies it structurally; kept framework-agnostic (mirrors `Queryable`
|
|
@@ -30,7 +31,13 @@ export interface ScopeVersionBumper {
|
|
|
30
31
|
export declare class ScopeClosureService {
|
|
31
32
|
private readonly pool;
|
|
32
33
|
private readonly bumper;
|
|
33
|
-
|
|
34
|
+
private readonly schemas;
|
|
35
|
+
/**
|
|
36
|
+
* @param schemas resolved framework names: the node tree (`companies`) lives in `schemas.app`,
|
|
37
|
+
* the closure table in `schemas.authz` (ADR-0011 / Task 1.1). Absent ⇒ resolved from the
|
|
38
|
+
* namespace, once, here.
|
|
39
|
+
*/
|
|
40
|
+
constructor(pool: ScopePool, bumper: ScopeVersionBumper, schemas?: IdentitySchemaNames);
|
|
34
41
|
/**
|
|
35
42
|
* Rebuild `scope_closure` from the current `companies.c_parent_uuid` edges. Idempotent
|
|
36
43
|
* (delete-then-insert in a tx → a second run over the same tree writes the identical set).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-closure.service.d.ts","sourceRoot":"","sources":["../../../src/features/scope/scope-closure.service.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"scope-closure.service.d.ts","sourceRoot":"","sources":["../../../src/features/scope/scope-closure.service.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAClF,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEpF;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED,iGAAiG;AACjG,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,aAAa,IAAI,OAAO,CACtB,SAAS,GAAG;QACV,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QACzB,2FAA2F;QAC3F,OAAO,CAAC,IAAI,IAAI,CAAC;KAClB,CACF,CAAC;CACH;AAOD,wFAAwF;AACxF,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,+FAA+F;IAC/F,gBAAgB,CAAC,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,qFAAqF;IACrF,cAAc,CAAC,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;CACxC;AAED,qBAAa,mBAAmB;IAO5B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B;;;;OAIG;gBAEgB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,kBAAkB,EAC1B,OAAO,GAAE,mBAA+C;IAG3E;;;;;;OAMG;IACG,gBAAgB,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAsBvF;;;;;;OAMG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcjD;;;;;;;OAOG;YACW,UAAU;IAwCxB;;;;;;;;;;;;;;;;OAgBG;IACG,QAAQ,CACZ,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GAAG,IAAI;IAC5B,+FAA+F;IAC/F,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GACjD,OAAO,CAAC,IAAI,CAAC;IAiFhB;;;;;;;;;;;;;;;;OAgBG;IACG,UAAU,CACd,QAAQ,EAAE,MAAM;IAChB,iGAAiG;IACjG,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC;IAClD;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,GAAG,SAAS,MAAM,EAAE,CAAC,GACpE,OAAO,CAAC,IAAI,CAAC;IA+ChB,yFAAyF;YAC3E,SAAS;IAOvB;;;;OAIG;YACW,cAAc;CAiB7B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG;IAC7D,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,CA8BA;AAED,kGAAkG;AAClG,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,QAAQ,EAAE,MAAM;CAQ7B"}
|
|
@@ -40,10 +40,7 @@ exports.quarantineCycles = quarantineCycles;
|
|
|
40
40
|
*/
|
|
41
41
|
const core_1 = require("@astram/core");
|
|
42
42
|
const guard_1 = require("@astram/guard");
|
|
43
|
-
|
|
44
|
-
const APP = 'shield_app';
|
|
45
|
-
/** Schema the closure table lives in (ADR-0011 / Task 1.1). */
|
|
46
|
-
const AUTHZ = 'shield_authz';
|
|
43
|
+
const schema_js_1 = require("../../schema.js");
|
|
47
44
|
/** The named lock every hierarchy writer holds (security review F3). */
|
|
48
45
|
const TREE_LOCK = 'shield_scope_closure';
|
|
49
46
|
/** Seconds to wait for it before failing closed. */
|
|
@@ -51,9 +48,16 @@ const TREE_LOCK_TIMEOUT_S = 10;
|
|
|
51
48
|
class ScopeClosureService {
|
|
52
49
|
pool;
|
|
53
50
|
bumper;
|
|
54
|
-
|
|
51
|
+
schemas;
|
|
52
|
+
/**
|
|
53
|
+
* @param schemas resolved framework names: the node tree (`companies`) lives in `schemas.app`,
|
|
54
|
+
* the closure table in `schemas.authz` (ADR-0011 / Task 1.1). Absent ⇒ resolved from the
|
|
55
|
+
* namespace, once, here.
|
|
56
|
+
*/
|
|
57
|
+
constructor(pool, bumper, schemas = (0, schema_js_1.frameworkSchemasFromEnv)()) {
|
|
55
58
|
this.pool = pool;
|
|
56
59
|
this.bumper = bumper;
|
|
60
|
+
this.schemas = schemas;
|
|
57
61
|
}
|
|
58
62
|
/**
|
|
59
63
|
* Rebuild `scope_closure` from the current `companies.c_parent_uuid` edges. Idempotent
|
|
@@ -183,7 +187,7 @@ class ScopeClosureService {
|
|
|
183
187
|
if (newParentUuid !== null) {
|
|
184
188
|
// Cycle guard: the new parent must NOT be at/under the moved node. A closure hit means
|
|
185
189
|
// newParent is a descendant of node → re-parenting node under it would form a cycle.
|
|
186
|
-
const cyclic = (await conn.query(`SELECT 1 FROM \`${
|
|
190
|
+
const cyclic = (await conn.query(`SELECT 1 FROM \`${this.schemas.authz}\`.\`scope_closure\`
|
|
187
191
|
WHERE ancestor_uuid = ? AND descendant_uuid = ? LIMIT 1`, [nodeUuid, newParentUuid]));
|
|
188
192
|
if (cyclic.length > 0) {
|
|
189
193
|
throw new guard_1.ScopeCycleError(nodeUuid);
|
|
@@ -191,14 +195,14 @@ class ScopeClosureService {
|
|
|
191
195
|
}
|
|
192
196
|
// The moved subtree — the nodes whose ancestry changes (node + all descendants). Read
|
|
193
197
|
// BEFORE the update (the move doesn't change the subtree's internal membership).
|
|
194
|
-
const subtreeRows = (await conn.query(`SELECT descendant_uuid FROM \`${
|
|
198
|
+
const subtreeRows = (await conn.query(`SELECT descendant_uuid FROM \`${this.schemas.authz}\`.\`scope_closure\` WHERE ancestor_uuid = ?`, [nodeUuid]));
|
|
195
199
|
// The FORMER-PARENT ANCESTRY — the nodes ABOVE `node` in the OLD tree (its ancestors,
|
|
196
200
|
// excluding self). They just LOST `node`'s subtree from their reach, so their admins' cached
|
|
197
201
|
// session subtrees are now stale (they'd otherwise keep reaching the moved-away tenant — the
|
|
198
202
|
// HIGH stale-subtree-after-move leak). Read BEFORE the update (the pre-move closure has the
|
|
199
203
|
// old ancestry). Bump these too so those sessions re-derive their (now-smaller) subtree at
|
|
200
204
|
// their next refresh (ADR-0011 bound — no forced re-login).
|
|
201
|
-
const formerAncestryRows = (await conn.query(`SELECT ancestor_uuid FROM \`${
|
|
205
|
+
const formerAncestryRows = (await conn.query(`SELECT ancestor_uuid FROM \`${this.schemas.authz}\`.\`scope_closure\`
|
|
202
206
|
WHERE descendant_uuid = ? AND ancestor_uuid <> ?`, [nodeUuid, nodeUuid]));
|
|
203
207
|
// Fall back to the node itself if the closure has no self-row yet (pre-first-reconcile).
|
|
204
208
|
const movedSubtree = subtreeRows.length > 0 ? subtreeRows.map((r) => r.descendant_uuid) : [nodeUuid];
|
|
@@ -209,10 +213,7 @@ class ScopeClosureService {
|
|
|
209
213
|
];
|
|
210
214
|
{
|
|
211
215
|
// 1) Re-parent the edge.
|
|
212
|
-
await conn.query(`UPDATE \`${
|
|
213
|
-
newParentUuid,
|
|
214
|
-
nodeUuid,
|
|
215
|
-
]);
|
|
216
|
+
await conn.query(`UPDATE \`${this.schemas.app}\`.\`companies\` SET c_parent_uuid = ? WHERE c_uuid = ?`, [newParentUuid, nodeUuid]);
|
|
216
217
|
// 2) Rebuild the whole closure from the now-updated edges (full rebuild — safe + simple;
|
|
217
218
|
// a targeted subtree rebuild is a later optimization). Re-reads edges over the SAME tx
|
|
218
219
|
// connection so it sees the UPDATE. Fail-loud propagates → the whole move rolls back.
|
|
@@ -267,20 +268,22 @@ class ScopeClosureService {
|
|
|
267
268
|
// Under the tree lock (review F3): a concurrent move cannot hang a child under this node
|
|
268
269
|
// between the children check and the delete.
|
|
269
270
|
await this.inTreeLock(async (conn) => {
|
|
270
|
-
const children = (await conn.query(`SELECT 1 FROM \`${
|
|
271
|
+
const children = (await conn.query(`SELECT 1 FROM \`${this.schemas.app}\`.\`companies\` WHERE c_parent_uuid = ? LIMIT 1`, [nodeUuid]));
|
|
271
272
|
if (children.length > 0) {
|
|
272
273
|
throw new NodeHasChildrenError(nodeUuid);
|
|
273
274
|
}
|
|
274
275
|
const touched = (beforeDelete ? await beforeDelete(conn) : undefined) ?? [];
|
|
275
276
|
// The former-parent ancestry — the deleted node's ancestors (excluding self). They lose the
|
|
276
277
|
// node from their reach → bump so their sessions re-derive. Read BEFORE the delete.
|
|
277
|
-
const formerAncestryRows = (await conn.query(`SELECT ancestor_uuid FROM \`${
|
|
278
|
+
const formerAncestryRows = (await conn.query(`SELECT ancestor_uuid FROM \`${this.schemas.authz}\`.\`scope_closure\`
|
|
278
279
|
WHERE descendant_uuid = ? AND ancestor_uuid <> ?`, [nodeUuid, nodeUuid]));
|
|
279
280
|
// Remove the node's closure rows (self-row + any it appears in as ancestor/descendant).
|
|
280
|
-
await conn.query(`DELETE FROM \`${
|
|
281
|
+
await conn.query(`DELETE FROM \`${this.schemas.authz}\`.\`scope_closure\`
|
|
281
282
|
WHERE ancestor_uuid = ? OR descendant_uuid = ?`, [nodeUuid, nodeUuid]);
|
|
282
283
|
// Remove the company row itself.
|
|
283
|
-
await conn.query(`DELETE FROM \`${
|
|
284
|
+
await conn.query(`DELETE FROM \`${this.schemas.app}\`.\`companies\` WHERE c_uuid = ?`, [
|
|
285
|
+
nodeUuid,
|
|
286
|
+
]);
|
|
284
287
|
// Bump the former-parent ancestry so their cached subtrees re-derive at next refresh, and the
|
|
285
288
|
// deleted node's OWN tenant (Phase 8 #3): its memberships and postings cascade away, so
|
|
286
289
|
// sessions cached in it must reload (and find nothing). A leaf has no descendants to bump.
|
|
@@ -299,7 +302,7 @@ class ScopeClosureService {
|
|
|
299
302
|
}
|
|
300
303
|
/** Read the node forest's parent edges (`c_uuid` → `c_parent_uuid`) as `ScopeEdge[]`. */
|
|
301
304
|
async readEdges(q) {
|
|
302
|
-
const rows = (await q.query(`SELECT c_uuid, c_parent_uuid FROM \`${
|
|
305
|
+
const rows = (await q.query(`SELECT c_uuid, c_parent_uuid FROM \`${this.schemas.app}\`.\`companies\``));
|
|
303
306
|
return rows.map((r) => ({ node: r.c_uuid, parent: r.c_parent_uuid ?? null }));
|
|
304
307
|
}
|
|
305
308
|
/**
|
|
@@ -308,7 +311,7 @@ class ScopeClosureService {
|
|
|
308
311
|
* idempotent (a re-run writes the identical target set) and correct for a full rebuild.
|
|
309
312
|
*/
|
|
310
313
|
async rewriteClosure(conn, rows) {
|
|
311
|
-
await conn.query(`DELETE FROM \`${
|
|
314
|
+
await conn.query(`DELETE FROM \`${this.schemas.authz}\`.\`scope_closure\``);
|
|
312
315
|
if (rows.length === 0)
|
|
313
316
|
return;
|
|
314
317
|
const placeholders = rows.map(() => '(?, ?, ?)').join(', ');
|
|
@@ -316,7 +319,7 @@ class ScopeClosureService {
|
|
|
316
319
|
for (const r of rows) {
|
|
317
320
|
params.push(r.ancestor, r.descendant, r.depth);
|
|
318
321
|
}
|
|
319
|
-
await conn.query(`INSERT INTO \`${
|
|
322
|
+
await conn.query(`INSERT INTO \`${this.schemas.authz}\`.\`scope_closure\` (ancestor_uuid, descendant_uuid, depth)
|
|
320
323
|
VALUES ${placeholders}`, params);
|
|
321
324
|
}
|
|
322
325
|
}
|