@astram/identity 0.2.1 → 0.3.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/company-membership.object.d.ts.map +1 -1
- package/dist/company-membership.object.js +7 -0
- package/dist/company.object.d.ts.map +1 -1
- package/dist/company.object.js +5 -0
- package/dist/features/authz/catalog-bootstrap.provider.d.ts +6 -1
- package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -1
- package/dist/features/authz/catalog-bootstrap.provider.js +28 -4
- package/dist/features/authz/grant-admin.controller.d.ts +4 -3
- package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.controller.js +22 -9
- package/dist/features/authz/grant-admin.service.d.ts +19 -3
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +43 -5
- package/dist/features/authz/record-authorization.d.ts +11 -2
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +11 -3
- package/dist/features/cross-tenant/grant-authorize.d.ts.map +1 -1
- package/dist/features/cross-tenant/grant-authorize.js +3 -0
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +136 -4
- package/dist/features/membership/membership-checker.d.ts +43 -0
- package/dist/features/membership/membership-checker.d.ts.map +1 -0
- package/dist/features/membership/membership-checker.js +25 -0
- package/dist/features/policy/policy.controller.d.ts +46 -0
- package/dist/features/policy/policy.controller.d.ts.map +1 -0
- package/dist/features/policy/policy.controller.js +150 -0
- package/dist/features/policy/policy.service.d.ts +129 -0
- package/dist/features/policy/policy.service.d.ts.map +1 -0
- package/dist/features/policy/policy.service.js +142 -0
- package/dist/features/scope/scope-closure.service.d.ts +82 -0
- package/dist/features/scope/scope-closure.service.d.ts.map +1 -0
- package/dist/features/scope/scope-closure.service.js +222 -0
- package/dist/features/scope/scope-subtree.resolver.d.ts +47 -0
- package/dist/features/scope/scope-subtree.resolver.d.ts.map +1 -0
- package/dist/features/scope/scope-subtree.resolver.js +24 -0
- package/dist/features/share/share.controller.d.ts +35 -0
- package/dist/features/share/share.controller.d.ts.map +1 -0
- package/dist/features/share/share.controller.js +143 -0
- package/dist/features/share/share.service.d.ts +90 -0
- package/dist/features/share/share.service.d.ts.map +1 -0
- package/dist/features/share/share.service.js +58 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/membership-posting.object.d.ts.map +1 -1
- package/dist/membership-posting.object.js +10 -0
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +2 -1
- package/dist/record-share.object.d.ts +24 -0
- package/dist/record-share.object.d.ts.map +1 -0
- package/dist/record-share.object.js +60 -0
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +10 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +10 -0
- package/package.json +4 -4
|
@@ -33,9 +33,14 @@ const db_permission_facts_adapter_js_1 = require("./authz/db-permission-facts.ad
|
|
|
33
33
|
const access_bundle_store_js_1 = require("./authz/access-bundle-store.js");
|
|
34
34
|
const grant_admin_service_js_1 = require("./authz/grant-admin.service.js");
|
|
35
35
|
const grant_admin_controller_js_1 = require("./authz/grant-admin.controller.js");
|
|
36
|
+
const policy_service_js_1 = require("./policy/policy.service.js");
|
|
37
|
+
const policy_controller_js_1 = require("./policy/policy.controller.js");
|
|
38
|
+
const share_service_js_1 = require("./share/share.service.js");
|
|
39
|
+
const share_controller_js_1 = require("./share/share.controller.js");
|
|
36
40
|
const catalog_reconcile_job_js_1 = require("./authz/catalog-reconcile.job.js");
|
|
37
41
|
const access_bundle_reconcile_job_js_1 = require("./authz/access-bundle-reconcile.job.js");
|
|
38
42
|
const catalog_bootstrap_provider_js_1 = require("./authz/catalog-bootstrap.provider.js");
|
|
43
|
+
const scope_closure_service_js_1 = require("./scope/scope-closure.service.js");
|
|
39
44
|
const mariadb_audit_reader_js_1 = require("./audit/mariadb-audit-reader.js");
|
|
40
45
|
const directory_label_resolver_js_1 = require("./audit/directory-label-resolver.js");
|
|
41
46
|
const audit_read_service_js_1 = require("./audit/audit-read.service.js");
|
|
@@ -91,6 +96,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
91
96
|
// postingVersionBump closure pattern above).
|
|
92
97
|
const accessBundleStore = new access_bundle_store_js_1.AccessBundleStore(authzPool);
|
|
93
98
|
const grantAdminService = new grant_admin_service_js_1.GrantAdminService(authzPool, bumper, accessBundleStore);
|
|
99
|
+
// Scope closure (Task 1.2 of the scoped-hierarchy program): the reconciler + node
|
|
100
|
+
// move/delete maintenance, over the SAME shared authzPool (it reaches BOTH shield_app —
|
|
101
|
+
// companies — and shield_authz — scope_closure). Reuses the SAME `bumper` instance so a
|
|
102
|
+
// moveNode's session-invalidation joins the move's transaction. reconcileClosure() is
|
|
103
|
+
// kicked post-boot by the CatalogBootstrapProvider (mirrors the catalog reconcile).
|
|
104
|
+
const scopeClosureService = new scope_closure_service_js_1.ScopeClosureService(authzPool, bumper);
|
|
94
105
|
// The core ModuleRegistry the catalog reconcile job needs, built from the identity
|
|
95
106
|
// object registrations (guard's bridge reflects decorated classes → registry).
|
|
96
107
|
const identityRegistry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects]);
|
|
@@ -113,6 +124,14 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
113
124
|
// same record-authorization inputs (matches astram3): the route guard's own decide() is the
|
|
114
125
|
// first; this one backs the writer's server-side re-verification.
|
|
115
126
|
const crossTenantAuthority = (0, guard_1.buildRecordAuthorization)((0, record_authorization_js_1.buildIdentityRecordAuthorization)(authzPool));
|
|
127
|
+
// Record-level LIST SCOPE (Task 3.1, Invariant #8): the engine-facing fn the engine's
|
|
128
|
+
// `list()` ANDs into BOTH the data SELECT and the COUNT — the in-query visible-set filter.
|
|
129
|
+
// Built HERE, over the SAME shared authzPool (guard constructs the un-overridable scope
|
|
130
|
+
// adapter inside `buildListScopeFn`, mirroring the four record-tier read adapters). Wired
|
|
131
|
+
// out-of-the-box into `EngineModule.forRoot({ listScope })` below (ADR-0003 injection seam —
|
|
132
|
+
// the engine never imports guard). Fail-closed: a store outage propagates and `list()` fails
|
|
133
|
+
// rather than returning unscoped rows.
|
|
134
|
+
const listScope = (0, guard_1.buildListScopeFn)((0, record_authorization_js_1.buildIdentityRecordAuthorization)(authzPool));
|
|
116
135
|
// The writer's `GrantAuthorizeFn`: decide()-backed, fail-closed (no try/catch — a throw
|
|
117
136
|
// propagates and the writer maps it to a retryable 503). isRoot ← the SAME rootIds the
|
|
118
137
|
// consumer passes to ShieldModule.forRoot (root-correctness for control-plane grants).
|
|
@@ -168,6 +187,101 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
168
187
|
// The read side (inbox/mine/incoming/offerable-types) over the same pool + the merged object
|
|
169
188
|
// set (offerable = tenantScoped types only) + the per-type action vocabulary.
|
|
170
189
|
const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects], guard_1.actionsFor);
|
|
190
|
+
// ── GUIDED RULE-BUILDER (list-scoping engine Phase 5, §9.1) ──────────────────────────────
|
|
191
|
+
// The policy-authoring surface over the guard `MariaDbPolicyStore` + core `makeAuthorPolicy`.
|
|
192
|
+
// Reuses the SAME `identityRegistry` + `crossTenantAuthority` (a DecisionAuthority over the
|
|
193
|
+
// shared authzPool — the delegated-admin ceiling's decide()) as the cross-tenant writer. The
|
|
194
|
+
// core AuditSink is one-arg (`record(event)`); the engine `MariaDbAuditSink` is two-arg
|
|
195
|
+
// (`record(event, query)`) — so bind the query to the shared authzPool (mirrors the
|
|
196
|
+
// `grantAudit` bridge above). Best-effort audit (a sink throw must not fail the author).
|
|
197
|
+
const policyAuditSink = new engine_1.MariaDbAuditSink({}); // shield_audit.audit_log defaults
|
|
198
|
+
const policyAudit = {
|
|
199
|
+
record: async (event) => {
|
|
200
|
+
try {
|
|
201
|
+
await policyAuditSink.record(event, (sql, params) => authzPool.query(sql, params));
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Best-effort: an audit failure must not fail a legitimate policy author.
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const policyStore = new guard_1.MariaDbPolicyStore(authzPool);
|
|
209
|
+
// The guided-builder SCHEMA source — the authorable field/action metadata, reflected from the
|
|
210
|
+
// registered identity object types (engine `reflectObjectType` yields fields WITH their
|
|
211
|
+
// `type`, which the core registry descriptor drops). Only tenantScoped, owned DATA-plane types
|
|
212
|
+
// are meaningfully policy-authorable in v1; but we expose every registered type's fields and
|
|
213
|
+
// let the delegated-admin ceiling + validateGuidedPolicy gate what a caller may actually
|
|
214
|
+
// author. A consumer's own types (extraObjects) would extend this the same way (future).
|
|
215
|
+
const schemaSource = [...objects_js_1.identityObjects].map((input) => {
|
|
216
|
+
const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
|
|
217
|
+
return {
|
|
218
|
+
typeKey: reg.typeKey,
|
|
219
|
+
label: reg.typeKey,
|
|
220
|
+
actions: (0, guard_1.actionsFor)(reg.typeKey),
|
|
221
|
+
fields: reg.fields.map((f) => ({ key: f.key, type: f.type })),
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
const policyService = new policy_service_js_1.PolicyService({
|
|
225
|
+
registry: identityRegistry,
|
|
226
|
+
policyStore,
|
|
227
|
+
decide: crossTenantAuthority,
|
|
228
|
+
audit: policyAudit,
|
|
229
|
+
schemaSource,
|
|
230
|
+
});
|
|
231
|
+
// ── OWNER-SHARE surface (list-scoping engine Phase 6, §9.2) ──────────────────────────────
|
|
232
|
+
// The framework-owned RecordShareWriter (the who-may-share bound) + a ShareTypeResolver over
|
|
233
|
+
// the reflected identity object types (so guard never imports @astram/engine — the consumer
|
|
234
|
+
// resolves the physical share info here). Only `owned` types are shareable; the resolver
|
|
235
|
+
// surfaces `owned`/table/schema/pk/owner/tenant columns via the engine helpers. The writer's
|
|
236
|
+
// audit reuses the SAME shield_audit sink pattern (best-effort). The ShareService adds
|
|
237
|
+
// name-resolution (no raw uuid) via the existing DirectoryLabelResolver (`auditLabels`).
|
|
238
|
+
const shareTypeInfoByKey = new Map();
|
|
239
|
+
for (const input of objects_js_1.identityObjects) {
|
|
240
|
+
const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
|
|
241
|
+
const owner = (0, engine_1.ownerColumn)(reg);
|
|
242
|
+
const pk = reg.primaryKey.kind === 'uuid' ? (0, engine_1.uuidPkColumn)(reg.primaryKey) : null;
|
|
243
|
+
// Only OWNED types with a uuid PK + an owner column are shareable; others are omitted (the
|
|
244
|
+
// writer rejects an unknown/non-owned type anyway — this map only lists real candidates).
|
|
245
|
+
if (!(0, engine_1.isOwned)(reg) || owner === null || pk === null)
|
|
246
|
+
continue;
|
|
247
|
+
const tenantCol = (0, engine_1.tenantColumnOf)(reg);
|
|
248
|
+
shareTypeInfoByKey.set(reg.typeKey, {
|
|
249
|
+
table: reg.table,
|
|
250
|
+
...(reg.schema ? { schema: reg.schema } : {}),
|
|
251
|
+
pkColumn: pk,
|
|
252
|
+
ownerColumn: owner,
|
|
253
|
+
...(tenantCol ? { tenantColumn: tenantCol } : {}),
|
|
254
|
+
owned: true,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const shareTypeResolver = {
|
|
258
|
+
resolve: (typeKey) => shareTypeInfoByKey.get(typeKey),
|
|
259
|
+
};
|
|
260
|
+
const shareAuditSink = new engine_1.MariaDbAuditSink({}); // shield_audit.audit_log
|
|
261
|
+
const recordShareWriter = new guard_1.RecordShareWriter({
|
|
262
|
+
db: authzPool,
|
|
263
|
+
types: shareTypeResolver,
|
|
264
|
+
audit: {
|
|
265
|
+
record: async (event) => {
|
|
266
|
+
try {
|
|
267
|
+
await shareAuditSink.record({
|
|
268
|
+
// A share/unshare is access administration → 'admin_action' (a valid AuditEventType;
|
|
269
|
+
// 'record_share' is the writer's own event tag, carried in `action`/attributes).
|
|
270
|
+
type: 'admin_action',
|
|
271
|
+
occurredAtMs: event.occurredAtMs,
|
|
272
|
+
actorId: event.actorId,
|
|
273
|
+
tenantId: event.tenantId,
|
|
274
|
+
action: { key: event.action }, // 'share.created' | 'share.removed'
|
|
275
|
+
attributes: { ...event.attributes, eventType: 'record_share' },
|
|
276
|
+
}, (sql, params) => authzPool.query(sql, params));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Best-effort — an audit failure must never fail a legitimate share/unshare.
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const shareService = new share_service_js_1.ShareService(recordShareWriter, auditLabels);
|
|
171
285
|
return {
|
|
172
286
|
module: IdentityFeatureModule_1,
|
|
173
287
|
imports: [
|
|
@@ -179,7 +293,11 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
179
293
|
// snapshot is empty and no bundles reconcile into shield_authz.access_bundle.
|
|
180
294
|
engine_1.EngineModule.forFeature([], { accessBundles: index_js_1.identityAccessBundles }),
|
|
181
295
|
// The engine's runtime CRUD pipe + the identity object registrations.
|
|
182
|
-
engine_1.EngineModule.forRoot({
|
|
296
|
+
engine_1.EngineModule.forRoot({
|
|
297
|
+
db: engineDbFromEnv(),
|
|
298
|
+
registrations: [...objects_js_1.identityObjects],
|
|
299
|
+
listScope,
|
|
300
|
+
}),
|
|
183
301
|
// NestJS DiscoveryService/MetadataScanner — the boot bootstrap provider reflects the
|
|
184
302
|
// live controllers through this to build the OperationInventory.
|
|
185
303
|
core_1.DiscoveryModule,
|
|
@@ -200,6 +318,10 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
200
318
|
// controller's own method order keeps the static sub-paths (/inbox, /offerable-types)
|
|
201
319
|
// ahead of `/:uuid/*`. Appended after the existing controllers.
|
|
202
320
|
cross_tenant_grant_controller_js_1.CrossTenantGrantController,
|
|
321
|
+
// Guided rule-builder (Phase 5) — `record-policies/*` collides with nothing. Appended last.
|
|
322
|
+
policy_controller_js_1.PolicyController,
|
|
323
|
+
// Owner shares (Phase 6) — `record-shares` collides with nothing. Appended last.
|
|
324
|
+
share_controller_js_1.ShareController,
|
|
203
325
|
],
|
|
204
326
|
providers: [
|
|
205
327
|
company_service_js_1.CompanyService,
|
|
@@ -221,6 +343,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
221
343
|
// wiring (the out-of-the-box acceptance bar).
|
|
222
344
|
{ provide: guard_1.SHIELD_CROSS_TENANT_GRANT_WRITER, useValue: crossTenantGrantWriter },
|
|
223
345
|
{ provide: cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService, useValue: crossTenantGrantReadService },
|
|
346
|
+
// Guided rule-builder (Phase 5) — the PolicyService over the shared authzPool + registry
|
|
347
|
+
// + the SAME DecisionAuthority (delegated-admin ceiling). useValue like GrantAdminService.
|
|
348
|
+
{ provide: policy_service_js_1.PolicyService, useValue: policyService },
|
|
349
|
+
// Owner shares (Phase 6) — the ShareService over the framework RecordShareWriter + the
|
|
350
|
+
// DirectoryLabelResolver (name resolution). useValue, all over the shared authzPool.
|
|
351
|
+
{ provide: share_service_js_1.ShareService, useValue: shareService },
|
|
224
352
|
// The mutating half of auto-catalog: a single CatalogReconcileJob (post-boot pass +
|
|
225
353
|
// cron), over the shared pool + the identity registry.
|
|
226
354
|
{ provide: catalog_reconcile_job_js_1.CatalogReconcileJob, useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, identityRegistry) },
|
|
@@ -231,10 +359,14 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
231
359
|
useFactory: (bundles) => new access_bundle_reconcile_job_js_1.AccessBundleReconcileJob(authzPool, bundles),
|
|
232
360
|
inject: [engine_1.ENGINE_ALL_ACCESS_BUNDLES],
|
|
233
361
|
},
|
|
362
|
+
// Scope closure reconciler + node move/delete maintenance (Task 1.2), built at forRoot()
|
|
363
|
+
// over the shared authzPool + the shared bumper, provided useValue so the bootstrap
|
|
364
|
+
// provider (and any future controller) injects the SAME instance.
|
|
365
|
+
{ provide: scope_closure_service_js_1.ScopeClosureService, useValue: scopeClosureService },
|
|
234
366
|
// Boot: read-only verifyGatable (fail-closed). Post-boot: kicks the jobs once (catalog
|
|
235
|
-
// THEN bundles) + schedules them (DISABLE_ALL_CRONS-
|
|
236
|
-
// MetadataScanner/Reflector (from DiscoveryModule
|
|
237
|
-
//
|
|
367
|
+
// THEN bundles, THEN the scope-closure reconcile) + schedules them (DISABLE_ALL_CRONS-
|
|
368
|
+
// gated). Injects DiscoveryService/MetadataScanner/Reflector (from DiscoveryModule
|
|
369
|
+
// above), the jobs, the ScopeClosureService, and ENGINE_ALL_REGISTRATIONS.
|
|
238
370
|
catalog_bootstrap_provider_js_1.CatalogBootstrapProvider,
|
|
239
371
|
],
|
|
240
372
|
exports: [company_service_js_1.CompanyService, role_service_js_1.RoleService, user_service_js_1.UserService],
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DbMembershipChecker` — the identity-side implementation of the guard-defined
|
|
3
|
+
* `MembershipChecker` port (Task 5.2 of the scoped-hierarchy tenancy program,
|
|
4
|
+
* ADR-0027 / ADR-0024). It answers ONE question the scope-switch gate asks: does a
|
|
5
|
+
* principal HOLD an ACTIVE membership at a target node? A `true` is the ONLY thing
|
|
6
|
+
* that lets `POST /auth/scope-switch` re-bind the session to that node; anything else
|
|
7
|
+
* (a `false`, or a thrown store outage the guard swallows) is a fail-closed DENY.
|
|
8
|
+
*
|
|
9
|
+
* The answer is a SINGLE existence read over `shield_app.company_membership`:
|
|
10
|
+
*
|
|
11
|
+
* SELECT 1 FROM shield_app.company_membership
|
|
12
|
+
* WHERE cm_user_uuid = ? AND cm_company_uuid = ?
|
|
13
|
+
* AND cm_is_active = 1 AND cm_status = 'active' LIMIT 1
|
|
14
|
+
*
|
|
15
|
+
* `cm_is_active = 1` is the first-class active-membership flag (a user has at most one
|
|
16
|
+
* active membership per company); `cm_status = 'active'` is the lifecycle status. BOTH
|
|
17
|
+
* are required — a de-activated OR non-active-status membership is NOT a valid switch
|
|
18
|
+
* target (least privilege: the switch confers the node's subtree + authority, so the
|
|
19
|
+
* binding must be genuinely live).
|
|
20
|
+
*
|
|
21
|
+
* ── LAYERING ────────────────────────────────────────────────────────────────────────
|
|
22
|
+
* The PORT (`MembershipChecker`) is defined in `@astram/guard`; this IMPLEMENTATION
|
|
23
|
+
* lives in `@astram/identity` (which owns the app schema + `company_membership`). The
|
|
24
|
+
* consumer injects an instance through `ShieldModule.forRoot({ membershipChecker })` —
|
|
25
|
+
* the same sanctioned direction as `DbScopeSubtreeResolver`. Guard never imports identity.
|
|
26
|
+
*
|
|
27
|
+
* ── FAIL-CLOSED ─────────────────────────────────────────────────────────────────────
|
|
28
|
+
* Returns `true` ONLY when the existence row is present. A store OUTAGE THROWS (the
|
|
29
|
+
* guard's scope-switch handler catches it → DENY): a check failure NEVER silently allows
|
|
30
|
+
* a switch. All SQL values are `?`-bound (never interpolated).
|
|
31
|
+
*/
|
|
32
|
+
import type { Pool } from 'mariadb';
|
|
33
|
+
export declare class DbMembershipChecker {
|
|
34
|
+
private readonly pool;
|
|
35
|
+
constructor(pool: Pool);
|
|
36
|
+
/**
|
|
37
|
+
* Does `principalId` hold an ACTIVE membership at `nodeId`? A single existence read —
|
|
38
|
+
* `true` iff a row with `cm_is_active = 1 AND cm_status = 'active'` exists. Throws on a
|
|
39
|
+
* store outage (the guard's scope-switch gate catches it → fail-closed DENY).
|
|
40
|
+
*/
|
|
41
|
+
hasActiveMembership(principalId: string, nodeId: string): Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=membership-checker.d.ts.map
|
|
@@ -0,0 +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;AAKpC,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEvC;;;;OAIG;IACG,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAWjF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbMembershipChecker = void 0;
|
|
4
|
+
/** Schema the membership table lives in (the app schema). */
|
|
5
|
+
const APP = 'shield_app';
|
|
6
|
+
class DbMembershipChecker {
|
|
7
|
+
pool;
|
|
8
|
+
constructor(pool) {
|
|
9
|
+
this.pool = pool;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Does `principalId` hold an ACTIVE membership at `nodeId`? A single existence read —
|
|
13
|
+
* `true` iff a row with `cm_is_active = 1 AND cm_status = 'active'` exists. Throws on a
|
|
14
|
+
* store outage (the guard's scope-switch gate catches it → fail-closed DENY).
|
|
15
|
+
*/
|
|
16
|
+
async hasActiveMembership(principalId, nodeId) {
|
|
17
|
+
const rows = (await this.pool.query(`SELECT 1 AS ok
|
|
18
|
+
FROM \`${APP}\`.\`company_membership\`
|
|
19
|
+
WHERE cm_user_uuid = ? AND cm_company_uuid = ?
|
|
20
|
+
AND cm_is_active = 1 AND cm_status = 'active'
|
|
21
|
+
LIMIT 1`, [principalId, nodeId]));
|
|
22
|
+
return rows.length > 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.DbMembershipChecker = DbMembershipChecker;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { GuidedPolicy, Principal } from '@astram/core';
|
|
2
|
+
import { PolicyService } from './policy.service.js';
|
|
3
|
+
/** The request shape after the enforcement guard attaches the resolved principal. */
|
|
4
|
+
interface AuthedRequest {
|
|
5
|
+
principal: Principal;
|
|
6
|
+
}
|
|
7
|
+
export declare class PolicyController {
|
|
8
|
+
private readonly policies;
|
|
9
|
+
constructor(policies: PolicyService);
|
|
10
|
+
/**
|
|
11
|
+
* The guided-builder SCHEMA for a type — its authorable fields (each with the operators the
|
|
12
|
+
* builder should offer, split into array/nullary families) + the type's actions.
|
|
13
|
+
* `GET /identity/record-policies/schema?typeKey=<key>`. Gated `role:read`. Server-derived so the
|
|
14
|
+
* builder's dropdowns never drift from enforcement. Unknown/non-authorable type → 404.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ Registered BEFORE `record-policies` (the `/schema` static sub-path) — Nest matches the
|
|
17
|
+
* first, and `record-policies/schema` is more specific than the bare `record-policies` GET.
|
|
18
|
+
*/
|
|
19
|
+
schema(typeKey?: string): Promise<import("./policy.service.js").PolicyTypeSchema>;
|
|
20
|
+
/**
|
|
21
|
+
* List the record-access policies governing a type in the caller's tenant.
|
|
22
|
+
* `GET /identity/record-policies?typeKey=<key>`. Gated `role:read`.
|
|
23
|
+
*/
|
|
24
|
+
list(req: AuthedRequest, typeKey?: string): Promise<{
|
|
25
|
+
policies: readonly import("@astram/core").Policy[];
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Author a guided policy. `POST /identity/record-policies` with a {@link GuidedPolicy} body
|
|
29
|
+
* (typeKey, action, effect, structured conditions — NO raw expressions). Gated `role:update`;
|
|
30
|
+
* the delegated-admin ceiling + structural validation run inside the service. The target tenant
|
|
31
|
+
* is `req.principal.tenantId` — a body tenant is ignored.
|
|
32
|
+
*/
|
|
33
|
+
author(req: AuthedRequest, body: GuidedPolicy): Promise<{
|
|
34
|
+
policy: import("@astram/core").Policy;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Delete a policy the caller owns. `DELETE /identity/record-policies/:id`. Gated `role:update`.
|
|
38
|
+
* TENANT-SCOPED: a foreign tenant's policy id → NotFound (the service can never delete another
|
|
39
|
+
* tenant's policy).
|
|
40
|
+
*/
|
|
41
|
+
delete(req: AuthedRequest, id: string): Promise<{
|
|
42
|
+
success: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=policy.controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.controller.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.controller.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,aAAa,EAId,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,qBACa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,aAAa;IAEpD;;;;;;;;OAQG;IAGG,MAAM,CAAmB,OAAO,CAAC,EAAE,MAAM;IAY/C;;;OAGG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAoB,OAAO,CAAC,EAAE,MAAM;;;IAQxE;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,YAAY;;;IAWlE;;;;OAIG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM;;;CAShE"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.PolicyController = void 0;
|
|
16
|
+
/**
|
|
17
|
+
* `PolicyController` — the §9.1 GUIDED RULE-BUILDER authoring API (list-scoping engine Phase 5).
|
|
18
|
+
*
|
|
19
|
+
* Three routes over {@link PolicyService}; gated purely by `@Classified` + `decide()` (no
|
|
20
|
+
* handler-level root check). Authoring a record-access policy IS access administration — the same
|
|
21
|
+
* authority as managing grants — so it keys to the `role` object type (chosen 2026-07-23): read
|
|
22
|
+
* routes need `identity:role:read`, mutating routes need `identity:role:update`. `role` is a
|
|
23
|
+
* CONTROL-plane type, so the capped root is allowed without a grant; a non-root admin needs it.
|
|
24
|
+
* On TOP of the route gate, the delegated-admin ceiling (only-author-what-you-can-grant) is
|
|
25
|
+
* enforced INSIDE the service (core `makeAuthorPolicy` → `checkDelegatedAdmin`).
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ Path is `record-policies` (NOT under `roles/...`) so it never collides with
|
|
28
|
+
* IdentityController's `roles/:uuid` or GrantAdminController's `access/*` routes.
|
|
29
|
+
*
|
|
30
|
+
* Authority is ALWAYS `req.principal` (the enforcement guard attached it) — a tenant in the body
|
|
31
|
+
* is IGNORED by the service (the target is `actor.tenantId`), so cross-tenant authoring is
|
|
32
|
+
* impossible by construction.
|
|
33
|
+
*/
|
|
34
|
+
const common_1 = require("@nestjs/common");
|
|
35
|
+
const guard_1 = require("@astram/guard");
|
|
36
|
+
const policy_service_js_1 = require("./policy.service.js");
|
|
37
|
+
let PolicyController = class PolicyController {
|
|
38
|
+
policies;
|
|
39
|
+
constructor(policies) {
|
|
40
|
+
this.policies = policies;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The guided-builder SCHEMA for a type — its authorable fields (each with the operators the
|
|
44
|
+
* builder should offer, split into array/nullary families) + the type's actions.
|
|
45
|
+
* `GET /identity/record-policies/schema?typeKey=<key>`. Gated `role:read`. Server-derived so the
|
|
46
|
+
* builder's dropdowns never drift from enforcement. Unknown/non-authorable type → 404.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ Registered BEFORE `record-policies` (the `/schema` static sub-path) — Nest matches the
|
|
49
|
+
* first, and `record-policies/schema` is more specific than the bare `record-policies` GET.
|
|
50
|
+
*/
|
|
51
|
+
async schema(typeKey) {
|
|
52
|
+
if (!typeKey || typeof typeKey !== 'string') {
|
|
53
|
+
throw new common_1.BadRequestException('typeKey required');
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return this.policies.schema(typeKey);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e instanceof policy_service_js_1.PolicyNotFoundError)
|
|
60
|
+
throw new common_1.NotFoundException(e.message);
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* List the record-access policies governing a type in the caller's tenant.
|
|
66
|
+
* `GET /identity/record-policies?typeKey=<key>`. Gated `role:read`.
|
|
67
|
+
*/
|
|
68
|
+
async list(req, typeKey) {
|
|
69
|
+
if (!typeKey || typeof typeKey !== 'string') {
|
|
70
|
+
throw new common_1.BadRequestException('typeKey required');
|
|
71
|
+
}
|
|
72
|
+
const policies = await this.policies.list(req.principal, typeKey);
|
|
73
|
+
return { policies };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Author a guided policy. `POST /identity/record-policies` with a {@link GuidedPolicy} body
|
|
77
|
+
* (typeKey, action, effect, structured conditions — NO raw expressions). Gated `role:update`;
|
|
78
|
+
* the delegated-admin ceiling + structural validation run inside the service. The target tenant
|
|
79
|
+
* is `req.principal.tenantId` — a body tenant is ignored.
|
|
80
|
+
*/
|
|
81
|
+
async author(req, body) {
|
|
82
|
+
try {
|
|
83
|
+
const policy = await this.policies.author(req.principal, body);
|
|
84
|
+
return { policy };
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (e instanceof policy_service_js_1.InvalidPolicyError)
|
|
88
|
+
throw new common_1.BadRequestException(e.message);
|
|
89
|
+
if (e instanceof policy_service_js_1.PolicyAuthorDeniedError)
|
|
90
|
+
throw new common_1.ForbiddenException(e.message);
|
|
91
|
+
throw e; // transient (store/authority outage) → propagates → 503
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Delete a policy the caller owns. `DELETE /identity/record-policies/:id`. Gated `role:update`.
|
|
96
|
+
* TENANT-SCOPED: a foreign tenant's policy id → NotFound (the service can never delete another
|
|
97
|
+
* tenant's policy).
|
|
98
|
+
*/
|
|
99
|
+
async delete(req, id) {
|
|
100
|
+
try {
|
|
101
|
+
await this.policies.delete(req.principal, id);
|
|
102
|
+
return { success: true };
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (e instanceof policy_service_js_1.PolicyNotFoundError)
|
|
106
|
+
throw new common_1.NotFoundException(e.message);
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
exports.PolicyController = PolicyController;
|
|
112
|
+
__decorate([
|
|
113
|
+
(0, common_1.Get)('record-policies/schema'),
|
|
114
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
115
|
+
__param(0, (0, common_1.Query)('typeKey')),
|
|
116
|
+
__metadata("design:type", Function),
|
|
117
|
+
__metadata("design:paramtypes", [String]),
|
|
118
|
+
__metadata("design:returntype", Promise)
|
|
119
|
+
], PolicyController.prototype, "schema", null);
|
|
120
|
+
__decorate([
|
|
121
|
+
(0, common_1.Get)('record-policies'),
|
|
122
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
123
|
+
__param(0, (0, common_1.Req)()),
|
|
124
|
+
__param(1, (0, common_1.Query)('typeKey')),
|
|
125
|
+
__metadata("design:type", Function),
|
|
126
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
127
|
+
__metadata("design:returntype", Promise)
|
|
128
|
+
], PolicyController.prototype, "list", null);
|
|
129
|
+
__decorate([
|
|
130
|
+
(0, common_1.Post)('record-policies'),
|
|
131
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
132
|
+
__param(0, (0, common_1.Req)()),
|
|
133
|
+
__param(1, (0, common_1.Body)()),
|
|
134
|
+
__metadata("design:type", Function),
|
|
135
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
136
|
+
__metadata("design:returntype", Promise)
|
|
137
|
+
], PolicyController.prototype, "author", null);
|
|
138
|
+
__decorate([
|
|
139
|
+
(0, common_1.Delete)('record-policies/:id'),
|
|
140
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
141
|
+
__param(0, (0, common_1.Req)()),
|
|
142
|
+
__param(1, (0, common_1.Param)('id')),
|
|
143
|
+
__metadata("design:type", Function),
|
|
144
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
145
|
+
__metadata("design:returntype", Promise)
|
|
146
|
+
], PolicyController.prototype, "delete", null);
|
|
147
|
+
exports.PolicyController = PolicyController = __decorate([
|
|
148
|
+
(0, common_1.Controller)('identity'),
|
|
149
|
+
__metadata("design:paramtypes", [policy_service_js_1.PolicyService])
|
|
150
|
+
], PolicyController);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PolicyService` — the backend of the §9.1 GUIDED RULE-BUILDER (list-scoping engine Phase 5).
|
|
3
|
+
*
|
|
4
|
+
* A thin authoring surface over the core `makeAuthorPolicy` command + the guard
|
|
5
|
+
* `MariaDbPolicyStore`. The core command already provides — and this service NEVER re-implements
|
|
6
|
+
* — validation (`validateGuidedPolicy`: known field, operator-valid-for-field-type, well-formed
|
|
7
|
+
* value; NO raw expressions), the delegated-admin ceiling (`checkDelegatedAdmin`: a non-root
|
|
8
|
+
* admin may author a policy ONLY over an `(action, typeKey)` they THEMSELVES hold — reusing the
|
|
9
|
+
* ONE decision authority), and the immutable audit record. This service:
|
|
10
|
+
* • builds the `AdminMutation` from the SERVER-resolved principal — the target tenant is
|
|
11
|
+
* ALWAYS `actor.tenantId`, NEVER the request body (a body tenant is ignored → a cross-tenant
|
|
12
|
+
* author is impossible by construction);
|
|
13
|
+
* • maps the core `CommandResult` deny reasons to typed errors the controller renders as
|
|
14
|
+
* 4xx/5xx (invalid → 400, denied/exceeds-authority → 403, outage → 503);
|
|
15
|
+
* • lists policies TENANT-SCOPED (`listPolicies(typeKey, actor.tenantId)`);
|
|
16
|
+
* • deletes policies TENANT-SCOPED (`deletePolicyForTenant` — a foreign tenant's policy id can
|
|
17
|
+
* NEVER be deleted; absent/foreign → NotFound, never a false success).
|
|
18
|
+
*
|
|
19
|
+
* FAIL-CLOSED: a transient store/authority throw PROPAGATES (the controller maps it to 503) —
|
|
20
|
+
* NEVER swallowed into a success. The delegated-admin check and validation run BEFORE any write.
|
|
21
|
+
*/
|
|
22
|
+
import type { AuditSink, DecisionAuthority, GuidedPolicy, ModuleRegistry, Policy, Principal } from '@astram/core';
|
|
23
|
+
import type { MariaDbPolicyStore } from '@astram/guard';
|
|
24
|
+
/** A guided policy could not be authored because it is structurally invalid (→ 400). */
|
|
25
|
+
export declare class InvalidPolicyError extends Error {
|
|
26
|
+
constructor(message?: string);
|
|
27
|
+
}
|
|
28
|
+
/** The acting admin exceeds their authority (delegated-admin ceiling) — author denied (→ 403). */
|
|
29
|
+
export declare class PolicyAuthorDeniedError extends Error {
|
|
30
|
+
constructor(message?: string);
|
|
31
|
+
}
|
|
32
|
+
/** A policy id was not found in the acting admin's tenant (absent OR another tenant's) (→ 404). */
|
|
33
|
+
export declare class PolicyNotFoundError extends Error {
|
|
34
|
+
constructor(message?: string);
|
|
35
|
+
}
|
|
36
|
+
/** The minimal store surface the service uses (the concrete guard adapter satisfies it). */
|
|
37
|
+
export interface PolicyStoreLike {
|
|
38
|
+
savePolicy(tenantId: string, policy: GuidedPolicy): Promise<Policy>;
|
|
39
|
+
listPolicies(typeKey: string, tenantId: string): Promise<readonly Policy[]>;
|
|
40
|
+
deletePolicyForTenant(policyId: string, tenantId: string): Promise<number>;
|
|
41
|
+
}
|
|
42
|
+
/** The physical/logical type of a field, as the engine declares it. */
|
|
43
|
+
export type FieldTypeName = 'string' | 'number' | 'boolean' | 'uuid' | 'timestamp' | 'json';
|
|
44
|
+
/** One authorable field of a type, with the operators the guided builder should OFFER. */
|
|
45
|
+
export interface PolicyFieldSchema {
|
|
46
|
+
readonly key: string;
|
|
47
|
+
readonly label: string;
|
|
48
|
+
readonly type: FieldTypeName;
|
|
49
|
+
/** The operators VALID for this field type (the UI's dropdown; server validation is authority). */
|
|
50
|
+
readonly validOperators: readonly PolicyOperatorName[];
|
|
51
|
+
/** Operators requiring an array operand (in/not_in) — the UI renders a multi-value input. */
|
|
52
|
+
readonly arrayOperators: readonly PolicyOperatorName[];
|
|
53
|
+
/** Operators taking NO operand (is_null/is_not_null) — the UI hides the value input. */
|
|
54
|
+
readonly nullaryOperators: readonly PolicyOperatorName[];
|
|
55
|
+
}
|
|
56
|
+
/** The guided-builder schema for a type: its authorable fields + the type's actions. */
|
|
57
|
+
export interface PolicyTypeSchema {
|
|
58
|
+
readonly typeKey: string;
|
|
59
|
+
readonly label: string;
|
|
60
|
+
readonly actions: readonly string[];
|
|
61
|
+
readonly fields: readonly PolicyFieldSchema[];
|
|
62
|
+
}
|
|
63
|
+
/** The closed operator vocabulary — mirrors core `PolicyOperator`. */
|
|
64
|
+
export type PolicyOperatorName = 'eq' | 'ne' | 'in' | 'not_in' | 'gt' | 'lt' | 'gte' | 'lte' | 'contains' | 'not_contains' | 'is_null' | 'is_not_null';
|
|
65
|
+
/** A field descriptor the schema source hands the service (from the reflected object type). */
|
|
66
|
+
export interface FieldDef {
|
|
67
|
+
readonly key: string;
|
|
68
|
+
readonly type: FieldTypeName;
|
|
69
|
+
}
|
|
70
|
+
/** Per-type field + action metadata the service reads to build the guided-builder schema. */
|
|
71
|
+
export interface TypeSchemaSource {
|
|
72
|
+
readonly typeKey: string;
|
|
73
|
+
readonly label: string;
|
|
74
|
+
readonly actions: readonly string[];
|
|
75
|
+
readonly fields: readonly FieldDef[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The operators the guided builder offers for a FIELD TYPE. Equality + null apply to every type;
|
|
79
|
+
* relational (gt/lt/…) only to numeric + timestamp (the recorded v1 limit — lexical vs numeric
|
|
80
|
+
* ordering on a VARCHAR diverges, so relational is NOT offered on string/uuid); text
|
|
81
|
+
* (contains/…) only to string. boolean/json get equality + null only. This is INTENTIONALLY a
|
|
82
|
+
* subset the builder shows; the server accepts anything `operatorValidForField` allows.
|
|
83
|
+
*/
|
|
84
|
+
export declare function validOperatorsForFieldType(type: FieldTypeName): readonly PolicyOperatorName[];
|
|
85
|
+
/** The dependencies the service wires (all constructed in the identity module). */
|
|
86
|
+
export interface PolicyServiceDeps {
|
|
87
|
+
readonly registry: ModuleRegistry;
|
|
88
|
+
readonly policyStore: MariaDbPolicyStore | PolicyStoreLike;
|
|
89
|
+
readonly decide: DecisionAuthority;
|
|
90
|
+
readonly audit: AuditSink;
|
|
91
|
+
/**
|
|
92
|
+
* The authorable-type field/action metadata (built in the module from the reflected object
|
|
93
|
+
* types). Drives `schema()` — the guided builder's field + operator dropdowns. A type absent
|
|
94
|
+
* here is NOT authorable via the builder (schema → NotFound).
|
|
95
|
+
*/
|
|
96
|
+
readonly schemaSource?: readonly TypeSchemaSource[];
|
|
97
|
+
readonly now?: () => number;
|
|
98
|
+
}
|
|
99
|
+
export declare class PolicyService {
|
|
100
|
+
private readonly store;
|
|
101
|
+
private readonly authorPolicy;
|
|
102
|
+
private readonly schemaByType;
|
|
103
|
+
constructor(deps: PolicyServiceDeps);
|
|
104
|
+
/**
|
|
105
|
+
* The guided-builder SCHEMA for a type: its authorable fields (each with its type + the
|
|
106
|
+
* operators the builder offers, split into array/nullary families the UI renders differently)
|
|
107
|
+
* + the type's actions. Server-derived from the registered object type so the builder's
|
|
108
|
+
* dropdowns are ONE source of truth with enforcement (no client-side operator table to drift).
|
|
109
|
+
* A type not in the schema source → PolicyNotFoundError (not authorable via the builder).
|
|
110
|
+
*/
|
|
111
|
+
schema(typeKey: string): PolicyTypeSchema;
|
|
112
|
+
/**
|
|
113
|
+
* Author a guided policy in the acting admin's OWN tenant. The target tenant is
|
|
114
|
+
* `actor.tenantId` — a `tenantId` in the body is IGNORED (cross-tenant author impossible).
|
|
115
|
+
* Runs the core pipeline (delegated-admin ceiling → validate → write → audit). Maps deny
|
|
116
|
+
* reasons to typed errors; a transient throw PROPAGATES (→ 503).
|
|
117
|
+
*/
|
|
118
|
+
author(actor: Principal, policy: GuidedPolicy): Promise<Policy>;
|
|
119
|
+
/** List the policies governing `typeKey` in the acting admin's tenant (tenant-scoped read). */
|
|
120
|
+
list(actor: Principal, typeKey: string): Promise<readonly Policy[]>;
|
|
121
|
+
/**
|
|
122
|
+
* Delete a policy the acting admin owns. TENANT-SCOPED: `deletePolicyForTenant` removes rows
|
|
123
|
+
* only when they belong to `actor.tenantId`, so a foreign tenant's policy id can never be
|
|
124
|
+
* deleted. Zero rows removed ⇒ the policy is absent OR another tenant's ⇒ NotFound (never a
|
|
125
|
+
* false "deleted someone else's").
|
|
126
|
+
*/
|
|
127
|
+
delete(actor: Principal, policyId: string): Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=policy.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.service.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.service.ts"],"names":[],"mappings":"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"}
|