@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,
|
|
1
|
+
{"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,qBAca,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;IAGd,QAAQ,EAAG,MAAM,CAAC;IAGlB,WAAW,EAAG,MAAM,CAAC;IAGrB,QAAQ,EAAG,OAAO,CAAC;IAGnB,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
|
|
@@ -58,6 +58,13 @@ exports.CompanyMembership = CompanyMembership = __decorate([
|
|
|
58
58
|
table: 'company_membership',
|
|
59
59
|
schema: schema_1.SCHEMA,
|
|
60
60
|
access: { plane: 'data', system: true },
|
|
61
|
+
// Read-scope wall (ADR-0027, Task 4.1): a membership's company IS the tenant node
|
|
62
|
+
// (`cm_company_uuid`), so a non-root LIST/GET is bounded to memberships whose company is
|
|
63
|
+
// in the caller's reachable subtree. DECOUPLED from writes — NOT `trusted`, because
|
|
64
|
+
// marking `cm_company_uuid` trusted would force every new membership's company to the
|
|
65
|
+
// admin's OWN node, breaking adding a user to a SUB-company (a legit hierarchy op). This
|
|
66
|
+
// filters reads only. Root spans all.
|
|
67
|
+
readScope: { kind: 'nodeColumn', column: 'cm_company_uuid' },
|
|
61
68
|
}),
|
|
62
69
|
(0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'cm_uuid' })
|
|
63
70
|
], CompanyMembership);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company.object.d.ts","sourceRoot":"","sources":["../src/company.object.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,
|
|
1
|
+
{"version":3,"file":"company.object.d.ts","sourceRoot":"","sources":["../src/company.object.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,qBAmBa,OAAO;IAElB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAGd,UAAU,EAAG,MAAM,CAAC;IAGpB,IAAI,EAAG,MAAM,CAAC;IAGd,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
|
package/dist/company.object.js
CHANGED
|
@@ -59,6 +59,11 @@ exports.Company = Company = __decorate([
|
|
|
59
59
|
// `company` row carries no business data. Consistent with company_department/company_branch,
|
|
60
60
|
// which are already control (managing a company's org structure is administration too).
|
|
61
61
|
access: { plane: 'control', system: true },
|
|
62
|
+
// Read-scope wall (ADR-0027, Task 4.1): a company IS a tenant node — its own row carries
|
|
63
|
+
// the node (`c_uuid`), so a non-root LIST/GET is bounded to the caller's reachable subtree
|
|
64
|
+
// (`c_uuid` IN my subtree). DECOUPLED from writes (no `trusted` change) — this filters
|
|
65
|
+
// reads only. Root spans all.
|
|
66
|
+
readScope: { kind: 'nodeColumn', column: 'c_uuid' },
|
|
62
67
|
}),
|
|
63
68
|
(0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'c_uuid' })
|
|
64
69
|
], Company);
|
|
@@ -3,6 +3,7 @@ import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
|
|
|
3
3
|
import type { ObjectType } from '@astram/engine';
|
|
4
4
|
import { CatalogReconcileJob } from './catalog-reconcile.job.js';
|
|
5
5
|
import { AccessBundleReconcileJob } from './access-bundle-reconcile.job.js';
|
|
6
|
+
import { ScopeClosureService } from '../scope/scope-closure.service.js';
|
|
6
7
|
/**
|
|
7
8
|
* Boot: read-only verifyGatable (fail-closed — a stranded/unclassified route aborts
|
|
8
9
|
* boot). Post-boot: kick the mutating reconcile once, then on an interval (single
|
|
@@ -20,9 +21,13 @@ export declare class CatalogBootstrapProvider implements OnApplicationBootstrap
|
|
|
20
21
|
private readonly reflector;
|
|
21
22
|
private readonly job;
|
|
22
23
|
private readonly accessBundleJob;
|
|
24
|
+
private readonly scopeClosure;
|
|
23
25
|
private readonly registrations;
|
|
24
26
|
private timer;
|
|
25
|
-
constructor(discovery: DiscoveryService, scanner: MetadataScanner, reflector: Reflector, job: CatalogReconcileJob, accessBundleJob: AccessBundleReconcileJob, registrations: readonly ObjectType[]);
|
|
27
|
+
constructor(discovery: DiscoveryService, scanner: MetadataScanner, reflector: Reflector, job: CatalogReconcileJob, accessBundleJob: AccessBundleReconcileJob, scopeClosure: ScopeClosureService, registrations: readonly ObjectType[]);
|
|
26
28
|
onApplicationBootstrap(): Promise<void>;
|
|
29
|
+
/** Rebuild the scope closure; a failure is logged loudly (never silently swallowed) but does
|
|
30
|
+
* not abort boot or a cron tick — the reconcile is idempotent and retried next interval. */
|
|
31
|
+
private reconcileScopeClosure;
|
|
27
32
|
}
|
|
28
33
|
//# sourceMappingURL=catalog-bootstrap.provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog-bootstrap.provider.d.ts","sourceRoot":"","sources":["../../../src/features/authz/catalog-bootstrap.provider.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAO5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"catalog-bootstrap.provider.d.ts","sourceRoot":"","sources":["../../../src/features/authz/catalog-bootstrap.provider.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAO5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKxE;;;;;;;;;;GAUG;AACH,qBACa,wBAAyB,YAAW,sBAAsB;IAInE,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAVhC,OAAO,CAAC,KAAK,CAA+B;gBAGzB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,SAAS,EACpB,GAAG,EAAE,mBAAmB,EACxB,eAAe,EAAE,wBAAwB,EACzC,YAAY,EAAE,mBAAmB,EAEjC,aAAa,EAAE,SAAS,UAAU,EAAE;IAGjD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC7C;iGAC6F;YAC/E,qBAAqB;CAOpC"}
|
|
@@ -23,6 +23,7 @@ const guard_1 = require("@astram/guard");
|
|
|
23
23
|
const engine_1 = require("@astram/engine");
|
|
24
24
|
const catalog_reconcile_job_js_1 = require("./catalog-reconcile.job.js");
|
|
25
25
|
const access_bundle_reconcile_job_js_1 = require("./access-bundle-reconcile.job.js");
|
|
26
|
+
const scope_closure_service_js_1 = require("../scope/scope-closure.service.js");
|
|
26
27
|
const MODULE_KEY = 'identity';
|
|
27
28
|
const RECONCILE_INTERVAL_MS = 60 * 60 * 1000; // hourly; honors DISABLE_ALL_CRONS below
|
|
28
29
|
/**
|
|
@@ -42,14 +43,16 @@ let CatalogBootstrapProvider = class CatalogBootstrapProvider {
|
|
|
42
43
|
reflector;
|
|
43
44
|
job;
|
|
44
45
|
accessBundleJob;
|
|
46
|
+
scopeClosure;
|
|
45
47
|
registrations;
|
|
46
48
|
timer = null;
|
|
47
|
-
constructor(discovery, scanner, reflector, job, accessBundleJob, registrations) {
|
|
49
|
+
constructor(discovery, scanner, reflector, job, accessBundleJob, scopeClosure, registrations) {
|
|
48
50
|
this.discovery = discovery;
|
|
49
51
|
this.scanner = scanner;
|
|
50
52
|
this.reflector = reflector;
|
|
51
53
|
this.job = job;
|
|
52
54
|
this.accessBundleJob = accessBundleJob;
|
|
55
|
+
this.scopeClosure = scopeClosure;
|
|
53
56
|
this.registrations = registrations;
|
|
54
57
|
}
|
|
55
58
|
async onApplicationBootstrap() {
|
|
@@ -64,22 +67,43 @@ let CatalogBootstrapProvider = class CatalogBootstrapProvider {
|
|
|
64
67
|
// Bundles reconcile AFTER permissions (same post-boot pass): a bundle validates
|
|
65
68
|
// its keys against the live permission catalog, so permissions must reconcile first.
|
|
66
69
|
await this.accessBundleJob.runOnce(nowIso);
|
|
70
|
+
// Scope-closure reconcile (Task 1.2): rebuild `scope_closure` from the live company tree so
|
|
71
|
+
// it is consistent with `companies.c_parent_uuid` at boot (mirrors the catalog reconcile).
|
|
72
|
+
// Independent of the catalog/bundles (it derives from the node tree, not the permission
|
|
73
|
+
// catalog). Fail-loud but NON-blocking: a corrupt tree (cycle/orphan) surfaces here as a
|
|
74
|
+
// logged error rather than aborting boot — the reconcile is idempotent and retried on the
|
|
75
|
+
// interval below once the data is fixed.
|
|
76
|
+
await this.reconcileScopeClosure();
|
|
67
77
|
if (process.env.DISABLE_ALL_CRONS !== 'true') {
|
|
68
78
|
this.timer = setInterval(() => {
|
|
69
79
|
const tickIso = new Date().toISOString();
|
|
70
|
-
void this.job
|
|
80
|
+
void this.job
|
|
81
|
+
.runOnce(tickIso)
|
|
82
|
+
.then(() => this.accessBundleJob.runOnce(tickIso))
|
|
83
|
+
.then(() => this.reconcileScopeClosure());
|
|
71
84
|
}, RECONCILE_INTERVAL_MS);
|
|
72
85
|
this.timer.unref?.();
|
|
73
86
|
}
|
|
74
87
|
}
|
|
88
|
+
/** Rebuild the scope closure; a failure is logged loudly (never silently swallowed) but does
|
|
89
|
+
* not abort boot or a cron tick — the reconcile is idempotent and retried next interval. */
|
|
90
|
+
async reconcileScopeClosure() {
|
|
91
|
+
try {
|
|
92
|
+
await this.scopeClosure.reconcileClosure();
|
|
93
|
+
}
|
|
94
|
+
catch (cause) {
|
|
95
|
+
console.error('[scope] closure reconcile FAILED (tree may be corrupt):', cause);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
75
98
|
};
|
|
76
99
|
exports.CatalogBootstrapProvider = CatalogBootstrapProvider;
|
|
77
100
|
exports.CatalogBootstrapProvider = CatalogBootstrapProvider = __decorate([
|
|
78
101
|
(0, common_1.Injectable)(),
|
|
79
|
-
__param(
|
|
102
|
+
__param(6, (0, common_1.Inject)(engine_1.ENGINE_ALL_REGISTRATIONS)),
|
|
80
103
|
__metadata("design:paramtypes", [core_1.DiscoveryService,
|
|
81
104
|
core_1.MetadataScanner,
|
|
82
105
|
core_1.Reflector,
|
|
83
106
|
catalog_reconcile_job_js_1.CatalogReconcileJob,
|
|
84
|
-
access_bundle_reconcile_job_js_1.AccessBundleReconcileJob,
|
|
107
|
+
access_bundle_reconcile_job_js_1.AccessBundleReconcileJob,
|
|
108
|
+
scope_closure_service_js_1.ScopeClosureService, Array])
|
|
85
109
|
], CatalogBootstrapProvider);
|
|
@@ -28,7 +28,7 @@ export declare class GrantAdminController {
|
|
|
28
28
|
* would be captured by IdentityController's `@Get('roles/:uuid')` (registered first),
|
|
29
29
|
* routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
|
|
30
30
|
*/
|
|
31
|
-
grantsSummary(query: {
|
|
31
|
+
grantsSummary(req: AuthedRequest, query: {
|
|
32
32
|
page?: string;
|
|
33
33
|
limit?: string;
|
|
34
34
|
search?: string;
|
|
@@ -51,8 +51,9 @@ export declare class GrantAdminController {
|
|
|
51
51
|
}[];
|
|
52
52
|
wildcards: string[];
|
|
53
53
|
}>;
|
|
54
|
-
/** List a role's current grants.
|
|
55
|
-
|
|
54
|
+
/** List a role's current grants. Subtree-guarded (Task 4.2): a non-root caller reads a role's
|
|
55
|
+
* grants only if the role is bound within their reachable subtree, else `{ grants: [] }`. */
|
|
56
|
+
list(req: AuthedRequest, uuid: string): Promise<{
|
|
56
57
|
grants: string[];
|
|
57
58
|
}>;
|
|
58
59
|
/** Grant one permission key to a role. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant-admin.controller.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.controller.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAe,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"grant-admin.controller.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.controller.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAe,MAAM,0BAA0B,CAAC;AAG1E,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAaD;;;;;;;;;;;GAWG;AACH,qBACa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,iBAAiB;IAEtD;;;;;;;;OAQG;IAGG,aAAa,CACV,GAAG,EAAE,aAAa,EAEzB,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;;;IAcH,sDAAsD;IAGhD,OAAO;;;;;;;;;;IAIb;kGAC8F;IAGxF,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IAIjE,0CAA0C;IAGpC,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAYlG,mFAAmF;IAG7E,MAAM,CAAgB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;IAOnE;;;;OAIG;IAGG,OAAO;;;IAIb;;;;OAIG;IAGG,WAAW,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAYxG;;;;OAIG;IAGG,YAAY,CAAgB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;CAS1E"}
|
|
@@ -19,6 +19,16 @@ exports.GrantAdminController = void 0;
|
|
|
19
19
|
const common_1 = require("@nestjs/common");
|
|
20
20
|
const guard_1 = require("@astram/guard");
|
|
21
21
|
const grant_admin_service_js_1 = require("./grant-admin.service.js");
|
|
22
|
+
/**
|
|
23
|
+
* Derive the caller's subtree scope from the resolved principal (Task 4.2). Uses the SAME cached
|
|
24
|
+
* `scopeSubtreeNodeIds` the RecordEngine's list-scope uses (one source of truth). Fail-safe to the
|
|
25
|
+
* active node `[tenantId]` if the subtree is absent (self-node, non-widening — never a leak).
|
|
26
|
+
*/
|
|
27
|
+
function subtreeScopeOf(req) {
|
|
28
|
+
const p = req.principal;
|
|
29
|
+
const nodeIds = p.scopeSubtreeNodeIds?.length ? p.scopeSubtreeNodeIds : [p.tenantId];
|
|
30
|
+
return { nodeIds, isRoot: p.isRoot === true };
|
|
31
|
+
}
|
|
22
32
|
/**
|
|
23
33
|
* Grant administration — list/grant/revoke a role's permission keys. Gated purely on
|
|
24
34
|
* `@Classified({ typeKey: 'role', ... })` + `decide()` (G1: no handler-level root
|
|
@@ -45,7 +55,7 @@ let GrantAdminController = class GrantAdminController {
|
|
|
45
55
|
* would be captured by IdentityController's `@Get('roles/:uuid')` (registered first),
|
|
46
56
|
* routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
|
|
47
57
|
*/
|
|
48
|
-
async grantsSummary(query) {
|
|
58
|
+
async grantsSummary(req, query) {
|
|
49
59
|
const page = Number.parseInt(query?.page ?? '', 10);
|
|
50
60
|
const limit = Number.parseInt(query?.limit ?? '', 10);
|
|
51
61
|
const params = {
|
|
@@ -59,15 +69,16 @@ let GrantAdminController = class GrantAdminController {
|
|
|
59
69
|
params.search = query.search;
|
|
60
70
|
if (typeof query?.sortBy === 'string')
|
|
61
71
|
params.sortBy = query.sortBy;
|
|
62
|
-
return this.grants.listGrantsSummary(params);
|
|
72
|
+
return this.grants.listGrantsSummary(params, subtreeScopeOf(req));
|
|
63
73
|
}
|
|
64
74
|
/** List all grantable permission keys + wildcards. */
|
|
65
75
|
async catalog() {
|
|
66
76
|
return this.grants.listCatalog();
|
|
67
77
|
}
|
|
68
|
-
/** List a role's current grants.
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
/** List a role's current grants. Subtree-guarded (Task 4.2): a non-root caller reads a role's
|
|
79
|
+
* grants only if the role is bound within their reachable subtree, else `{ grants: [] }`. */
|
|
80
|
+
async list(req, uuid) {
|
|
81
|
+
return this.grants.listRoleGrants(uuid, subtreeScopeOf(req));
|
|
71
82
|
}
|
|
72
83
|
/** Grant one permission key to a role. */
|
|
73
84
|
async grant(req, uuid, body) {
|
|
@@ -138,9 +149,10 @@ exports.GrantAdminController = GrantAdminController;
|
|
|
138
149
|
__decorate([
|
|
139
150
|
(0, common_1.Get)('access/role-grants'),
|
|
140
151
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
141
|
-
__param(0, (0, common_1.
|
|
152
|
+
__param(0, (0, common_1.Req)()),
|
|
153
|
+
__param(1, (0, common_1.Query)()),
|
|
142
154
|
__metadata("design:type", Function),
|
|
143
|
-
__metadata("design:paramtypes", [Object]),
|
|
155
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
144
156
|
__metadata("design:returntype", Promise)
|
|
145
157
|
], GrantAdminController.prototype, "grantsSummary", null);
|
|
146
158
|
__decorate([
|
|
@@ -153,9 +165,10 @@ __decorate([
|
|
|
153
165
|
__decorate([
|
|
154
166
|
(0, common_1.Get)('roles/:uuid/permissions'),
|
|
155
167
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
156
|
-
__param(0, (0, common_1.
|
|
168
|
+
__param(0, (0, common_1.Req)()),
|
|
169
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
157
170
|
__metadata("design:type", Function),
|
|
158
|
-
__metadata("design:paramtypes", [String]),
|
|
171
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
159
172
|
__metadata("design:returntype", Promise)
|
|
160
173
|
], GrantAdminController.prototype, "list", null);
|
|
161
174
|
__decorate([
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Pool } from 'mariadb';
|
|
2
2
|
import { PermissionVersionBumper } from './permission-version.bumper.js';
|
|
3
3
|
import { AccessBundleStore } from './access-bundle-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* The caller's reachable subtree, threaded from `req.principal` (Task 4.2). It scopes the
|
|
6
|
+
* grant-admin reads so a caller sees ONLY roles bound within their subtree — the SAME cached
|
|
7
|
+
* `scopeSubtreeNodeIds` the RecordEngine's list-scope uses (one source of truth for "a node's
|
|
8
|
+
* subtree"). `isRoot` spans all tenants (the tenant registry is root's scope) → NO predicate.
|
|
9
|
+
* A non-root with an EMPTY `nodeIds` matches nothing → an empty list (fail-closed, not a leak).
|
|
10
|
+
*/
|
|
11
|
+
export interface SubtreeScope {
|
|
12
|
+
readonly nodeIds: readonly string[];
|
|
13
|
+
readonly isRoot: boolean;
|
|
14
|
+
}
|
|
4
15
|
/** A granted key was not in the catalog and is not a recognized wildcard. The
|
|
5
16
|
* controller maps this to HTTP 400 (distinct from a store outage → 503). */
|
|
6
17
|
export declare class BadKeyError extends Error {
|
|
@@ -45,7 +56,12 @@ export declare class GrantAdminService {
|
|
|
45
56
|
catalog: CatalogRow[];
|
|
46
57
|
wildcards: string[];
|
|
47
58
|
}>;
|
|
48
|
-
|
|
59
|
+
/**
|
|
60
|
+
* A role's current grants. Task 4.2: subtree-guarded — a non-root caller may read a role's
|
|
61
|
+
* grants ONLY IF that role is bound within their reachable subtree; otherwise `{ grants: [] }`
|
|
62
|
+
* (fail-closed — never leak another tenant's role's permissions by uuid). Root skips the guard.
|
|
63
|
+
*/
|
|
64
|
+
listRoleGrants(roleUuid: string, subtreeScope: SubtreeScope): Promise<{
|
|
49
65
|
grants: string[];
|
|
50
66
|
}>;
|
|
51
67
|
/** Validate the key against the catalog + wildcard set. Throws BadKeyError if unknown. */
|
|
@@ -90,13 +106,13 @@ export declare class GrantAdminService {
|
|
|
90
106
|
* page (a per-role aggregate not cheaply sortable in SQL). Defaults: page 1, limit 25,
|
|
91
107
|
* sort name asc.
|
|
92
108
|
*/
|
|
93
|
-
listGrantsSummary(params
|
|
109
|
+
listGrantsSummary(params: {
|
|
94
110
|
page?: number;
|
|
95
111
|
limit?: number;
|
|
96
112
|
search?: string;
|
|
97
113
|
sortBy?: string;
|
|
98
114
|
sortOrder?: 'asc' | 'desc';
|
|
99
|
-
}): Promise<{
|
|
115
|
+
} | undefined, subtreeScope: SubtreeScope): Promise<{
|
|
100
116
|
rows: RoleGrantSummary[];
|
|
101
117
|
total: number;
|
|
102
118
|
page: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,22 @@ const core_1 = require("@astram/core");
|
|
|
5
5
|
const access_bundle_store_js_1 = require("./access-bundle-store.js");
|
|
6
6
|
const db_permission_facts_adapter_js_1 = require("./db-permission-facts.adapter.js");
|
|
7
7
|
const AUTHZ = 'shield_authz';
|
|
8
|
+
const APP = 'shield_app';
|
|
9
|
+
/**
|
|
10
|
+
* Build the shared EXISTS-subtree fragment for a non-root caller. A role qualifies only if it
|
|
11
|
+
* is bound (`membership_posting → company_membership`) to a node in the caller's subtree. All
|
|
12
|
+
* node ids are `?`-bound (never interpolated → injection-free); the caller splices the returned
|
|
13
|
+
* `params` at the position where `sql` sits in the surrounding query. Root / empty scope callers
|
|
14
|
+
* are handled by the caller (root ⇒ no fragment; the engine guarantees non-empty subtrees).
|
|
15
|
+
*/
|
|
16
|
+
function subtreeExists(nodeIds) {
|
|
17
|
+
const ph = nodeIds.map(() => '?').join(', ');
|
|
18
|
+
const sql = ` AND EXISTS (` +
|
|
19
|
+
`SELECT 1 FROM \`${APP}\`.\`membership_posting\` mp` +
|
|
20
|
+
` JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid` +
|
|
21
|
+
` WHERE mp.mp_role_uuid = r.r_uuid AND cm.cm_company_uuid IN (${ph}))`;
|
|
22
|
+
return { sql, params: [...nodeIds] };
|
|
23
|
+
}
|
|
8
24
|
/** A granted key was not in the catalog and is not a recognized wildcard. The
|
|
9
25
|
* controller maps this to HTTP 400 (distinct from a store outage → 503). */
|
|
10
26
|
class BadKeyError extends Error {
|
|
@@ -48,8 +64,25 @@ class GrantAdminService {
|
|
|
48
64
|
];
|
|
49
65
|
return { catalog, wildcards };
|
|
50
66
|
}
|
|
51
|
-
|
|
67
|
+
/**
|
|
68
|
+
* A role's current grants. Task 4.2: subtree-guarded — a non-root caller may read a role's
|
|
69
|
+
* grants ONLY IF that role is bound within their reachable subtree; otherwise `{ grants: [] }`
|
|
70
|
+
* (fail-closed — never leak another tenant's role's permissions by uuid). Root skips the guard.
|
|
71
|
+
*/
|
|
72
|
+
async listRoleGrants(roleUuid, subtreeScope) {
|
|
52
73
|
try {
|
|
74
|
+
if (!subtreeScope.isRoot) {
|
|
75
|
+
const ph = subtreeScope.nodeIds.map(() => '?').join(', ');
|
|
76
|
+
// Empty subtree → no placeholders → the check can't match → fail-closed empty.
|
|
77
|
+
const bound = subtreeScope.nodeIds.length
|
|
78
|
+
? (await this.pool.query(`SELECT 1 FROM \`${APP}\`.\`membership_posting\` mp
|
|
79
|
+
JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
80
|
+
WHERE mp.mp_role_uuid = ? AND cm.cm_company_uuid IN (${ph})
|
|
81
|
+
LIMIT 1`, [roleUuid, ...subtreeScope.nodeIds]))
|
|
82
|
+
: [];
|
|
83
|
+
if (bound.length === 0)
|
|
84
|
+
return { grants: [] };
|
|
85
|
+
}
|
|
53
86
|
const rows = (await this.pool.query(`SELECT rp_perm_key FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? ORDER BY rp_perm_key`, [roleUuid]));
|
|
54
87
|
return { grants: rows.map((r) => r.rp_perm_key) };
|
|
55
88
|
}
|
|
@@ -203,7 +236,7 @@ class GrantAdminService {
|
|
|
203
236
|
* page (a per-role aggregate not cheaply sortable in SQL). Defaults: page 1, limit 25,
|
|
204
237
|
* sort name asc.
|
|
205
238
|
*/
|
|
206
|
-
async listGrantsSummary(params) {
|
|
239
|
+
async listGrantsSummary(params, subtreeScope) {
|
|
207
240
|
const page = Math.max(1, Math.trunc(params?.page ?? 1));
|
|
208
241
|
const limit = Math.min(200, Math.max(1, Math.trunc(params?.limit ?? 25)));
|
|
209
242
|
const offset = (page - 1) * limit;
|
|
@@ -214,6 +247,11 @@ class GrantAdminService {
|
|
|
214
247
|
const { catalog } = await this.listCatalog();
|
|
215
248
|
const allKeys = catalog.map((c) => c.key);
|
|
216
249
|
const { bundles } = await this.listBundles();
|
|
250
|
+
// Task 4.2 subtree scope: a non-root caller sees ONLY roles bound within their reachable
|
|
251
|
+
// subtree (the EXISTS predicate); root spans all tenants (no predicate). The scope clause
|
|
252
|
+
// sits FIRST in the WHERE (before the LIKE), so its ?-bound node params lead the arg list —
|
|
253
|
+
// identical on BOTH the count and page queries so they can never disagree.
|
|
254
|
+
const scope = subtreeScope.isRoot ? { sql: '', params: [] } : subtreeExists(subtreeScope.nodeIds);
|
|
217
255
|
// Distinct granted roles (a role appears iff it has ≥1 role_permission row), filtered
|
|
218
256
|
// by name search. Bind the LIKE term as a param (wildcards escaped).
|
|
219
257
|
const likeClause = search ? `AND r.r_name LIKE ?` : '';
|
|
@@ -221,7 +259,7 @@ class GrantAdminService {
|
|
|
221
259
|
const [{ total = 0 } = {}] = (await this.pool.query(`SELECT COUNT(DISTINCT rp.rp_role_uuid) AS total
|
|
222
260
|
FROM \`${AUTHZ}\`.\`role_permission\` rp
|
|
223
261
|
JOIN \`shield_app\`.\`roles\` r ON r.r_uuid = rp.rp_role_uuid
|
|
224
|
-
WHERE 1 = 1 ${likeClause}`, likeArgs));
|
|
262
|
+
WHERE 1 = 1${scope.sql} ${likeClause}`, [...scope.params, ...likeArgs]));
|
|
225
263
|
// The page of role uuids + name + latest-grant metadata. name/grantedAt sort in SQL;
|
|
226
264
|
// permCount falls back to a stable name sort here (re-sorted after aggregation).
|
|
227
265
|
const orderCol = sortBy === 'grantedAt' ? 'latest_at' : 'r.r_name';
|
|
@@ -230,10 +268,10 @@ class GrantAdminService {
|
|
|
230
268
|
MAX(rp.rp_granted_at) AS latest_at
|
|
231
269
|
FROM \`${AUTHZ}\`.\`role_permission\` rp
|
|
232
270
|
JOIN \`shield_app\`.\`roles\` r ON r.r_uuid = rp.rp_role_uuid
|
|
233
|
-
WHERE 1 = 1 ${likeClause}
|
|
271
|
+
WHERE 1 = 1${scope.sql} ${likeClause}
|
|
234
272
|
GROUP BY rp.rp_role_uuid, r.r_name
|
|
235
273
|
ORDER BY ${orderCol} ${dir}, r.r_name ASC
|
|
236
|
-
LIMIT ? OFFSET ?`, [...likeArgs, limit, offset]));
|
|
274
|
+
LIMIT ? OFFSET ?`, [...scope.params, ...likeArgs, limit, offset]));
|
|
237
275
|
if (pageRoles.length === 0) {
|
|
238
276
|
return { rows: [], total: Number(total), page, limit };
|
|
239
277
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type Pool } from 'mariadb';
|
|
13
13
|
import { type RecordAuthorizationInputs } from '@astram/guard';
|
|
14
|
+
import type { ObjectTypeInput } from '@astram/engine';
|
|
14
15
|
/**
|
|
15
16
|
* A lean `mariadb` pool for the authz grant-reads (built from `APP_DB_*` env via guard's
|
|
16
17
|
* `loadDbConfig`). Kept small — these are pre-gate coarse-grant reads, not the main CRUD pool.
|
|
@@ -24,8 +25,16 @@ export declare function createIdentityAuthzPool(): Pool;
|
|
|
24
25
|
* - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
|
|
25
26
|
* `company_membership`/`membership_posting`,
|
|
26
27
|
* - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
|
|
27
|
-
* - `registry` — bridged from `identityObjects`,
|
|
28
|
+
* - `registry` — bridged from `identityObjects` PLUS any consumer-supplied `extraObjects`,
|
|
28
29
|
* - `operationManifest` — empty (identity declares no trusted delegated operations).
|
|
30
|
+
*
|
|
31
|
+
* `extraObjects` (optional): a CONSUMER app that registers its OWN gated object types via
|
|
32
|
+
* `EngineModule.forFeature([...])` must ALSO pass those types here, so their actions enter the
|
|
33
|
+
* coarse-grant permission-facts registry (gate 5). Without this, a custom type's routes boot
|
|
34
|
+
* and list-scope correctly (gate 6) but their `<module>:<type>:<action>` permission keys are
|
|
35
|
+
* never projected into a principal's grants — so gate 5 denies them (deny-by-default). Passing
|
|
36
|
+
* the same object types the app registered with the engine closes that gap: the app's grants on
|
|
37
|
+
* those keys now project, and the framework still owns the four record-tier adapters + scope fn.
|
|
29
38
|
*/
|
|
30
|
-
export declare function buildIdentityRecordAuthorization(pool: Pool): RecordAuthorizationInputs;
|
|
39
|
+
export declare function buildIdentityRecordAuthorization(pool: Pool, extraObjects?: readonly ObjectTypeInput[]): RecordAuthorizationInputs;
|
|
31
40
|
//# sourceMappingURL=record-authorization.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-authorization.d.ts","sourceRoot":"","sources":["../../../src/features/authz/record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAA6C,KAAK,yBAAyB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"record-authorization.d.ts","sourceRoot":"","sources":["../../../src/features/authz/record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAA6C,KAAK,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1G,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAKtD;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAS3B"}
|
|
@@ -44,11 +44,19 @@ function createIdentityAuthzPool() {
|
|
|
44
44
|
* - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
|
|
45
45
|
* `company_membership`/`membership_posting`,
|
|
46
46
|
* - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
|
|
47
|
-
* - `registry` — bridged from `identityObjects`,
|
|
47
|
+
* - `registry` — bridged from `identityObjects` PLUS any consumer-supplied `extraObjects`,
|
|
48
48
|
* - `operationManifest` — empty (identity declares no trusted delegated operations).
|
|
49
|
+
*
|
|
50
|
+
* `extraObjects` (optional): a CONSUMER app that registers its OWN gated object types via
|
|
51
|
+
* `EngineModule.forFeature([...])` must ALSO pass those types here, so their actions enter the
|
|
52
|
+
* coarse-grant permission-facts registry (gate 5). Without this, a custom type's routes boot
|
|
53
|
+
* and list-scope correctly (gate 6) but their `<module>:<type>:<action>` permission keys are
|
|
54
|
+
* never projected into a principal's grants — so gate 5 denies them (deny-by-default). Passing
|
|
55
|
+
* the same object types the app registered with the engine closes that gap: the app's grants on
|
|
56
|
+
* those keys now project, and the framework still owns the four record-tier adapters + scope fn.
|
|
49
57
|
*/
|
|
50
|
-
function buildIdentityRecordAuthorization(pool) {
|
|
51
|
-
const registry = (0, guard_1.buildIdentityModuleRegistry)(objects_js_1.identityObjects);
|
|
58
|
+
function buildIdentityRecordAuthorization(pool, extraObjects = []) {
|
|
59
|
+
const registry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects, ...extraObjects]);
|
|
52
60
|
return {
|
|
53
61
|
pool,
|
|
54
62
|
permissionFacts: new db_permission_facts_adapter_js_1.DbPermissionFactsAdapter(pool, registry),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant-authorize.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/grant-authorize.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,WAAW,iCAAiC;IAChD,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IACtD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnF;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;CACnD;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,iCAAiC,GACtC,gBAAgB,
|
|
1
|
+
{"version":3,"file":"grant-authorize.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/grant-authorize.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,WAAW,iCAAiC;IAChD,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IACtD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnF;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;CACnD;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,iCAAiC,GACtC,gBAAgB,CAqBlB"}
|
|
@@ -7,6 +7,9 @@ function makeAuthorizeCrossTenantGrant(deps) {
|
|
|
7
7
|
const principal = {
|
|
8
8
|
kind: 'user',
|
|
9
9
|
id: principalId,
|
|
10
|
+
// Scope (ADR-0027) — INTERIM self-node default (Task 2.1): non-widening, = today's isolation.
|
|
11
|
+
activeScopeNodeId: tenantId,
|
|
12
|
+
scopeSubtreeNodeIds: [tenantId],
|
|
10
13
|
tenantId,
|
|
11
14
|
roleIds,
|
|
12
15
|
isRoot: deps.isRoot(principalId),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAqF5D;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAE;QAAE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAO,GAAG,aAAa;CAoTjF"}
|