@astram/identity 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-facts-keys.d.ts +3 -0
- package/dist/auth-facts-keys.d.ts.map +1 -0
- package/dist/auth-facts-keys.js +24 -0
- package/dist/cross-tenant-grant.object.d.ts +28 -0
- package/dist/cross-tenant-grant.object.d.ts.map +1 -0
- package/dist/cross-tenant-grant.object.js +64 -0
- package/dist/features/access/access-admin.access.d.ts +3 -0
- package/dist/features/access/access-admin.access.d.ts.map +1 -0
- package/dist/features/access/access-admin.access.js +27 -0
- package/dist/features/access/company-admin.access.d.ts +3 -0
- package/dist/features/access/company-admin.access.d.ts.map +1 -0
- package/dist/features/access/company-admin.access.js +31 -0
- package/dist/features/access/index.d.ts +15 -0
- package/dist/features/access/index.d.ts.map +1 -0
- package/dist/features/access/index.js +26 -0
- package/dist/features/access/user-management.access.d.ts +3 -0
- package/dist/features/access/user-management.access.d.ts.map +1 -0
- package/dist/features/access/user-management.access.js +35 -0
- package/dist/features/audit/audit-read.service.d.ts +92 -0
- package/dist/features/audit/audit-read.service.d.ts.map +1 -0
- package/dist/features/audit/audit-read.service.js +97 -0
- package/dist/features/audit/audit.controller.d.ts +35 -0
- package/dist/features/audit/audit.controller.d.ts.map +1 -0
- package/dist/features/audit/audit.controller.js +73 -0
- package/dist/features/audit/directory-label-resolver.d.ts +14 -0
- package/dist/features/audit/directory-label-resolver.d.ts.map +1 -0
- package/dist/features/audit/directory-label-resolver.js +46 -0
- package/dist/features/audit/mariadb-audit-reader.d.ts +46 -0
- package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -0
- package/dist/features/audit/mariadb-audit-reader.js +116 -0
- package/dist/features/authz/access-bundle-reconcile.job.d.ts +35 -0
- package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -0
- package/dist/features/authz/access-bundle-reconcile.job.js +84 -0
- package/dist/features/authz/access-bundle-store.d.ts +65 -0
- package/dist/features/authz/access-bundle-store.d.ts.map +1 -0
- package/dist/features/authz/access-bundle-store.js +154 -0
- package/dist/features/authz/catalog-bootstrap.provider.d.ts +28 -0
- package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -0
- package/dist/features/authz/catalog-bootstrap.provider.js +85 -0
- package/dist/features/authz/catalog-reconcile.job.d.ts +23 -0
- package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -0
- package/dist/features/authz/catalog-reconcile.job.js +47 -0
- package/dist/features/authz/db-permission-facts.adapter.d.ts +60 -0
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -0
- package/dist/features/authz/db-permission-facts.adapter.js +116 -0
- package/dist/features/authz/grant-admin.controller.d.ts +96 -0
- package/dist/features/authz/grant-admin.controller.d.ts.map +1 -0
- package/dist/features/authz/grant-admin.controller.js +209 -0
- package/dist/features/authz/grant-admin.service.d.ts +107 -0
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -0
- package/dist/features/authz/grant-admin.service.js +297 -0
- package/dist/features/authz/mariadb-catalog-store.d.ts +16 -0
- package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -0
- package/dist/features/authz/mariadb-catalog-store.js +44 -0
- package/dist/features/authz/permission-version.bumper.d.ts +47 -0
- package/dist/features/authz/permission-version.bumper.d.ts.map +1 -0
- package/dist/features/authz/permission-version.bumper.js +68 -0
- package/dist/features/authz/record-authorization.d.ts +31 -0
- package/dist/features/authz/record-authorization.d.ts.map +1 -0
- package/dist/features/authz/record-authorization.js +59 -0
- package/dist/features/authz/session-facts.reader.d.ts +41 -0
- package/dist/features/authz/session-facts.reader.d.ts.map +1 -0
- package/dist/features/authz/session-facts.reader.js +27 -0
- package/dist/features/company/company.controller.d.ts +37 -0
- package/dist/features/company/company.controller.d.ts.map +1 -0
- package/dist/features/company/company.controller.js +100 -0
- package/dist/features/company/company.service.d.ts +25 -0
- package/dist/features/company/company.service.d.ts.map +1 -0
- package/dist/features/company/company.service.js +53 -0
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts +126 -0
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts.map +1 -0
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.js +276 -0
- package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts +65 -0
- package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -0
- package/dist/features/cross-tenant/cross-tenant-grant.controller.js +250 -0
- package/dist/features/cross-tenant/grant-authorize.d.ts +42 -0
- package/dist/features/cross-tenant/grant-authorize.d.ts.map +1 -0
- package/dist/features/cross-tenant/grant-authorize.js +17 -0
- package/dist/features/identity-feature.module.d.ts +26 -0
- package/dist/features/identity-feature.module.d.ts.map +1 -0
- package/dist/features/identity-feature.module.js +247 -0
- package/dist/features/membership/membership.controller.d.ts +77 -0
- package/dist/features/membership/membership.controller.d.ts.map +1 -0
- package/dist/features/membership/membership.controller.js +174 -0
- package/dist/features/membership/membership.service.d.ts +73 -0
- package/dist/features/membership/membership.service.d.ts.map +1 -0
- package/dist/features/membership/membership.service.js +173 -0
- package/dist/features/membership/posting-version-bump.d.ts +10 -0
- package/dist/features/membership/posting-version-bump.d.ts.map +1 -0
- package/dist/features/membership/posting-version-bump.js +4 -0
- package/dist/features/membership/posting.service.d.ts +59 -0
- package/dist/features/membership/posting.service.d.ts.map +1 -0
- package/dist/features/membership/posting.service.js +181 -0
- package/dist/features/org-structure/org-catalog.controller.d.ts +14 -0
- package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -0
- package/dist/features/org-structure/org-catalog.controller.js +62 -0
- package/dist/features/org-structure/org-catalog.service.d.ts +19 -0
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -0
- package/dist/features/org-structure/org-catalog.service.js +53 -0
- package/dist/features/role/role.controller.d.ts +37 -0
- package/dist/features/role/role.controller.d.ts.map +1 -0
- package/dist/features/role/role.controller.js +104 -0
- package/dist/features/role/role.service.d.ts +24 -0
- package/dist/features/role/role.service.d.ts.map +1 -0
- package/dist/features/role/role.service.js +53 -0
- package/dist/features/shared/http.d.ts +15 -0
- package/dist/features/shared/http.d.ts.map +1 -0
- package/dist/features/shared/http.js +25 -0
- package/dist/features/shared/list-query.d.ts +44 -0
- package/dist/features/shared/list-query.d.ts.map +1 -0
- package/dist/features/shared/list-query.js +53 -0
- package/dist/features/shared/named-option.d.ts +6 -0
- package/dist/features/shared/named-option.d.ts.map +1 -0
- package/dist/features/shared/named-option.js +2 -0
- package/dist/features/shared/record-resolver.d.ts +11 -0
- package/dist/features/shared/record-resolver.d.ts.map +1 -0
- package/dist/features/shared/record-resolver.js +32 -0
- package/dist/features/user/user.controller.d.ts +39 -0
- package/dist/features/user/user.controller.d.ts.map +1 -0
- package/dist/features/user/user.controller.js +104 -0
- package/dist/features/user/user.service.d.ts +26 -0
- package/dist/features/user/user.service.d.ts.map +1 -0
- package/dist/features/user/user.service.js +53 -0
- package/dist/index.d.ts +34 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -33
- package/dist/objects.d.ts +13 -0
- package/dist/objects.d.ts.map +1 -0
- package/dist/objects.js +20 -0
- package/dist/shared-resource.object.d.ts +28 -0
- package/dist/shared-resource.object.d.ts.map +1 -0
- package/dist/shared-resource.object.js +63 -0
- package/migrations/1710000000010-CreateAuditLog.cjs +42 -0
- package/migrations/1710000000011-CreateCrossTenantGrant.cjs +66 -0
- package/migrations/1710000000012-SeedCrossTenantGrantPerms.cjs +86 -0
- package/migrations/1710000000013-CreateSharedResource.cjs +30 -0
- package/migrations/1710000000099-SeedDevIdentity.cjs +130 -0
- package/migrations/1710000000100-SeedDevTenantBAndRoot.cjs +212 -0
- package/package.json +4 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-facts-keys.d.ts","sourceRoot":"","sources":["../src/auth-facts-keys.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAEZ,CAAC;AAEzC,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAGd,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.identityControlPlaneKeys = exports.identityAuthActionKeys = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The framework identity permission-key universe, projected once from `identityObjects` via
|
|
6
|
+
* `@astram/guard`'s registry bridge. Exported for the consumer to pass into
|
|
7
|
+
* `ShieldModule.forRoot({ auth: true, identityActionKeys, identityControlPlaneKeys })` — so the
|
|
8
|
+
* auth-facts adapter (in guard) can expand `identity:*` grants + union root's control-plane keys
|
|
9
|
+
* WITHOUT guard importing this package (the sanctioned graph is identity → guard).
|
|
10
|
+
*
|
|
11
|
+
* ⚠ NO MODULE-INIT CYCLE: `identityObjects` is imported from the leaf `./objects.js` (NOT from
|
|
12
|
+
* `./index.js`, which re-exports THIS file). So the array is fully initialized before the
|
|
13
|
+
* `buildIdentityModuleRegistry(...)` call below runs at module-eval time.
|
|
14
|
+
*/
|
|
15
|
+
const guard_1 = require("@astram/guard");
|
|
16
|
+
const objects_js_1 = require("./objects.js");
|
|
17
|
+
const registry = (0, guard_1.buildIdentityModuleRegistry)(objects_js_1.identityObjects);
|
|
18
|
+
exports.identityAuthActionKeys = registry
|
|
19
|
+
.listActions()
|
|
20
|
+
.map((a) => a.permissionKey);
|
|
21
|
+
exports.identityControlPlaneKeys = registry
|
|
22
|
+
.listActions()
|
|
23
|
+
.filter((a) => a.plane === 'control')
|
|
24
|
+
.map((a) => a.permissionKey);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cross_tenant_grant` — an AUTHORITY-ONLY @ObjectType, never CRUD'd through the
|
|
3
|
+
* RecordEngine pipe. Its sole purpose is to give `cross_tenant_grant` an entry in the
|
|
4
|
+
* core `ModuleRegistry` (via `module-registry.bridge.ts`) so `decide()` can answer the
|
|
5
|
+
* "may this principal share/accept/revoke a cross-tenant grant?" question the SAME way
|
|
6
|
+
* it answers every other object-type gate — no bespoke authority rule needed.
|
|
7
|
+
*
|
|
8
|
+
* `plane: 'control'` — WHO-MAY-GRANT is who-may-act administration (root allowed), the
|
|
9
|
+
* same posture as `role`/`company`. `system: true` (never `owned`/`tenantScoped`) — the
|
|
10
|
+
* table's row-level owner/tenant columns (`ctg_owning_tenant_uuid`,
|
|
11
|
+
* `ctg_grantee_tenant_uuid`) already encode the tenants the writer needs; that isolation
|
|
12
|
+
* is enforced by `CrossTenantGrantWriter` itself, not by the pipe's owner/tenant baseline.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors `membership-posting.object.ts`'s shape (system, no trusted columns needed
|
|
15
|
+
* beyond the PK) — the engine's `validateAccess` requires `table` + at least one
|
|
16
|
+
* `@Field`; the PK satisfies that. Action vocabulary is NOT the CRUD `IDENTITY_ACTIONS`
|
|
17
|
+
* — see `@astram/guard`'s `identity-actions.ts`'s `actionsFor('cross_tenant_grant')` ->
|
|
18
|
+
* `CROSS_TENANT_GRANT_ACTIONS` (`share`/`accept`/`revoke`).
|
|
19
|
+
*
|
|
20
|
+
* Ported from astram3's frozen
|
|
21
|
+
* `server/src/features/authz/objects/cross-tenant-grant.object.ts`.
|
|
22
|
+
*/
|
|
23
|
+
export declare class CrossTenantGrant {
|
|
24
|
+
uuid: string;
|
|
25
|
+
owningTenantUuid: string;
|
|
26
|
+
granteeTenantUuid: string;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=cross-tenant-grant.object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-tenant-grant.object.d.ts","sourceRoot":"","sources":["../src/cross-tenant-grant.object.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAOa,gBAAgB;IAE3B,IAAI,EAAG,MAAM,CAAC;IAGd,gBAAgB,EAAG,MAAM,CAAC;IAG1B,iBAAiB,EAAG,MAAM,CAAC;CAC5B"}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.CrossTenantGrant = void 0;
|
|
13
|
+
const engine_1 = require("@astram/engine");
|
|
14
|
+
/** The physical schema `cross_tenant_grant` lives in (migration 1710000000011). */
|
|
15
|
+
const SCHEMA = 'shield_authz';
|
|
16
|
+
/**
|
|
17
|
+
* `cross_tenant_grant` — an AUTHORITY-ONLY @ObjectType, never CRUD'd through the
|
|
18
|
+
* RecordEngine pipe. Its sole purpose is to give `cross_tenant_grant` an entry in the
|
|
19
|
+
* core `ModuleRegistry` (via `module-registry.bridge.ts`) so `decide()` can answer the
|
|
20
|
+
* "may this principal share/accept/revoke a cross-tenant grant?" question the SAME way
|
|
21
|
+
* it answers every other object-type gate — no bespoke authority rule needed.
|
|
22
|
+
*
|
|
23
|
+
* `plane: 'control'` — WHO-MAY-GRANT is who-may-act administration (root allowed), the
|
|
24
|
+
* same posture as `role`/`company`. `system: true` (never `owned`/`tenantScoped`) — the
|
|
25
|
+
* table's row-level owner/tenant columns (`ctg_owning_tenant_uuid`,
|
|
26
|
+
* `ctg_grantee_tenant_uuid`) already encode the tenants the writer needs; that isolation
|
|
27
|
+
* is enforced by `CrossTenantGrantWriter` itself, not by the pipe's owner/tenant baseline.
|
|
28
|
+
*
|
|
29
|
+
* Mirrors `membership-posting.object.ts`'s shape (system, no trusted columns needed
|
|
30
|
+
* beyond the PK) — the engine's `validateAccess` requires `table` + at least one
|
|
31
|
+
* `@Field`; the PK satisfies that. Action vocabulary is NOT the CRUD `IDENTITY_ACTIONS`
|
|
32
|
+
* — see `@astram/guard`'s `identity-actions.ts`'s `actionsFor('cross_tenant_grant')` ->
|
|
33
|
+
* `CROSS_TENANT_GRANT_ACTIONS` (`share`/`accept`/`revoke`).
|
|
34
|
+
*
|
|
35
|
+
* Ported from astram3's frozen
|
|
36
|
+
* `server/src/features/authz/objects/cross-tenant-grant.object.ts`.
|
|
37
|
+
*/
|
|
38
|
+
let CrossTenantGrant = class CrossTenantGrant {
|
|
39
|
+
uuid;
|
|
40
|
+
owningTenantUuid;
|
|
41
|
+
granteeTenantUuid;
|
|
42
|
+
};
|
|
43
|
+
exports.CrossTenantGrant = CrossTenantGrant;
|
|
44
|
+
__decorate([
|
|
45
|
+
(0, engine_1.Field)({ column: 'ctg_uuid', type: 'uuid' }),
|
|
46
|
+
__metadata("design:type", String)
|
|
47
|
+
], CrossTenantGrant.prototype, "uuid", void 0);
|
|
48
|
+
__decorate([
|
|
49
|
+
(0, engine_1.Field)({ column: 'ctg_owning_tenant_uuid', type: 'uuid' }),
|
|
50
|
+
__metadata("design:type", String)
|
|
51
|
+
], CrossTenantGrant.prototype, "owningTenantUuid", void 0);
|
|
52
|
+
__decorate([
|
|
53
|
+
(0, engine_1.Field)({ column: 'ctg_grantee_tenant_uuid', type: 'uuid' }),
|
|
54
|
+
__metadata("design:type", String)
|
|
55
|
+
], CrossTenantGrant.prototype, "granteeTenantUuid", void 0);
|
|
56
|
+
exports.CrossTenantGrant = CrossTenantGrant = __decorate([
|
|
57
|
+
(0, engine_1.ObjectType)({
|
|
58
|
+
typeKey: 'cross_tenant_grant',
|
|
59
|
+
table: 'cross_tenant_grant',
|
|
60
|
+
schema: SCHEMA,
|
|
61
|
+
access: { plane: 'control', system: true },
|
|
62
|
+
}),
|
|
63
|
+
(0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'ctg_uuid' })
|
|
64
|
+
], CrossTenantGrant);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-admin.access.d.ts","sourceRoot":"","sources":["../../../src/features/access/access-admin.access.ts"],"names":[],"mappings":"AAOA,qBAOa,iBAAiB;CAAG"}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.AccessAdminAccess = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* `access-admin` — the role-administration seed bundle from design §6. Full role
|
|
12
|
+
* lifecycle (managing roles + the access granted to them). Every key resolves to a
|
|
13
|
+
* REAL registered `identity:<type>:<action>`.
|
|
14
|
+
*/
|
|
15
|
+
const engine_1 = require("@astram/engine");
|
|
16
|
+
let AccessAdminAccess = class AccessAdminAccess {
|
|
17
|
+
};
|
|
18
|
+
exports.AccessAdminAccess = AccessAdminAccess;
|
|
19
|
+
exports.AccessAdminAccess = AccessAdminAccess = __decorate([
|
|
20
|
+
(0, engine_1.AccessBundle)({
|
|
21
|
+
key: 'access-admin',
|
|
22
|
+
module: 'identity',
|
|
23
|
+
label: 'Access Administration',
|
|
24
|
+
description: 'Manage roles and the access granted to them.',
|
|
25
|
+
permissions: [{ typeKey: 'role', actions: ['read', 'create', 'update', 'delete'] }],
|
|
26
|
+
})
|
|
27
|
+
], AccessAdminAccess);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"company-admin.access.d.ts","sourceRoot":"","sources":["../../../src/features/access/company-admin.access.ts"],"names":[],"mappings":"AAOA,qBAWa,kBAAkB;CAAG"}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CompanyAdminAccess = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* `company-admin` — the company-administration seed bundle from design §6. Full
|
|
12
|
+
* company lifecycle plus the read-only membership lookups a company-admin form needs.
|
|
13
|
+
* Every key resolves to a REAL registered `identity:<type>:<action>`.
|
|
14
|
+
*/
|
|
15
|
+
const engine_1 = require("@astram/engine");
|
|
16
|
+
let CompanyAdminAccess = class CompanyAdminAccess {
|
|
17
|
+
};
|
|
18
|
+
exports.CompanyAdminAccess = CompanyAdminAccess;
|
|
19
|
+
exports.CompanyAdminAccess = CompanyAdminAccess = __decorate([
|
|
20
|
+
(0, engine_1.AccessBundle)({
|
|
21
|
+
key: 'company-admin',
|
|
22
|
+
module: 'identity',
|
|
23
|
+
label: 'Company Administration',
|
|
24
|
+
description: 'Create and manage companies and their memberships.',
|
|
25
|
+
permissions: [
|
|
26
|
+
{ typeKey: 'company', actions: ['read', 'create', 'update', 'delete'] },
|
|
27
|
+
{ typeKey: 'company_membership', actions: ['read'] },
|
|
28
|
+
{ typeKey: 'membership_posting', actions: ['read'] },
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
], CompanyAdminAccess);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity access-bundles barrel. Each `@AccessBundle` class is declared in its own
|
|
3
|
+
* `<bundle>.access.ts` (co-located with the feature it grants, per the design's
|
|
4
|
+
* "declare it where you build it" model). This barrel re-exports each class + the
|
|
5
|
+
* aggregate `identityAccessBundles` array passed to `EngineModule.forFeature(...,
|
|
6
|
+
* { accessBundles })`, which the engine collects into `ENGINE_ALL_ACCESS_BUNDLES` and
|
|
7
|
+
* the boot reconciler upserts into `shield_authz.access_bundle`.
|
|
8
|
+
*/
|
|
9
|
+
import { UserManagementAccess } from './user-management.access';
|
|
10
|
+
export { UserManagementAccess } from './user-management.access';
|
|
11
|
+
export { CompanyAdminAccess } from './company-admin.access';
|
|
12
|
+
export { AccessAdminAccess } from './access-admin.access';
|
|
13
|
+
/** All identity access bundles, collected into the engine at boot. */
|
|
14
|
+
export declare const identityAccessBundles: (typeof UserManagementAccess)[];
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/features/access/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAIhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,iCAIjC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.identityAccessBundles = exports.AccessAdminAccess = exports.CompanyAdminAccess = exports.UserManagementAccess = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Identity access-bundles barrel. Each `@AccessBundle` class is declared in its own
|
|
6
|
+
* `<bundle>.access.ts` (co-located with the feature it grants, per the design's
|
|
7
|
+
* "declare it where you build it" model). This barrel re-exports each class + the
|
|
8
|
+
* aggregate `identityAccessBundles` array passed to `EngineModule.forFeature(...,
|
|
9
|
+
* { accessBundles })`, which the engine collects into `ENGINE_ALL_ACCESS_BUNDLES` and
|
|
10
|
+
* the boot reconciler upserts into `shield_authz.access_bundle`.
|
|
11
|
+
*/
|
|
12
|
+
const user_management_access_1 = require("./user-management.access");
|
|
13
|
+
const company_admin_access_1 = require("./company-admin.access");
|
|
14
|
+
const access_admin_access_1 = require("./access-admin.access");
|
|
15
|
+
var user_management_access_2 = require("./user-management.access");
|
|
16
|
+
Object.defineProperty(exports, "UserManagementAccess", { enumerable: true, get: function () { return user_management_access_2.UserManagementAccess; } });
|
|
17
|
+
var company_admin_access_2 = require("./company-admin.access");
|
|
18
|
+
Object.defineProperty(exports, "CompanyAdminAccess", { enumerable: true, get: function () { return company_admin_access_2.CompanyAdminAccess; } });
|
|
19
|
+
var access_admin_access_2 = require("./access-admin.access");
|
|
20
|
+
Object.defineProperty(exports, "AccessAdminAccess", { enumerable: true, get: function () { return access_admin_access_2.AccessAdminAccess; } });
|
|
21
|
+
/** All identity access bundles, collected into the engine at boot. */
|
|
22
|
+
exports.identityAccessBundles = [
|
|
23
|
+
user_management_access_1.UserManagementAccess,
|
|
24
|
+
company_admin_access_1.CompanyAdminAccess,
|
|
25
|
+
access_admin_access_1.AccessAdminAccess,
|
|
26
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-management.access.d.ts","sourceRoot":"","sources":["../../../src/features/access/user-management.access.ts"],"names":[],"mappings":"AAQA,qBAca,oBAAoB;CAAG"}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.UserManagementAccess = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* `user-management` — the cross-type seed bundle from design §3.1. Lets an admin
|
|
12
|
+
* grant "manage users" as ONE named unit: full user lifecycle plus the read-only
|
|
13
|
+
* lookups the user-admin forms need (role/department/branch) plus company-membership
|
|
14
|
+
* administration. Every key resolves to a REAL registered `identity:<type>:<action>`.
|
|
15
|
+
*/
|
|
16
|
+
const engine_1 = require("@astram/engine");
|
|
17
|
+
let UserManagementAccess = class UserManagementAccess {
|
|
18
|
+
};
|
|
19
|
+
exports.UserManagementAccess = UserManagementAccess;
|
|
20
|
+
exports.UserManagementAccess = UserManagementAccess = __decorate([
|
|
21
|
+
(0, engine_1.AccessBundle)({
|
|
22
|
+
key: 'user-management',
|
|
23
|
+
module: 'identity',
|
|
24
|
+
label: 'User Management',
|
|
25
|
+
description: 'Create, edit and assign users to companies.',
|
|
26
|
+
permissions: [
|
|
27
|
+
{ typeKey: 'user', actions: ['read', 'create', 'update', 'delete'] },
|
|
28
|
+
{ typeKey: 'role', actions: ['read'] },
|
|
29
|
+
{ typeKey: 'department', actions: ['read'] },
|
|
30
|
+
{ typeKey: 'branch', actions: ['read'] },
|
|
31
|
+
{ typeKey: 'company_membership', actions: ['read', 'create', 'update', 'delete'] },
|
|
32
|
+
{ typeKey: 'membership_posting', actions: ['read', 'update'] },
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
], UserManagementAccess);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { AuditEvent, LabelResolver } from '@astram/core';
|
|
2
|
+
export interface AuditRow {
|
|
3
|
+
readonly occurredAt: number;
|
|
4
|
+
readonly actorLabel: string;
|
|
5
|
+
readonly action: string;
|
|
6
|
+
readonly tenantLabel: string;
|
|
7
|
+
readonly detail: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface AuditFilters {
|
|
10
|
+
readonly type?: string;
|
|
11
|
+
readonly actorName?: string;
|
|
12
|
+
readonly tenantFilter?: string;
|
|
13
|
+
readonly before?: string;
|
|
14
|
+
readonly limit?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface AuditPrincipal {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly tenantId: string;
|
|
19
|
+
readonly isRoot: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** name -> actorId reverse lookup (for the actor filter). Returns null when unknown. */
|
|
22
|
+
export type ActorNameResolver = (name: string) => Promise<string | null>;
|
|
23
|
+
/**
|
|
24
|
+
* The tenant/type/actor SCOPE query shared by BOTH the page read and the total count. Building
|
|
25
|
+
* ONE of these and feeding it into both is what guarantees a non-root's total counts ONLY its
|
|
26
|
+
* own tenant (invariant #4: tenant isolation — the count must never widen past the page). `type`
|
|
27
|
+
* carries the action-key filter as the concrete reader's `AuditEvent['type']`.
|
|
28
|
+
*/
|
|
29
|
+
export interface CountQuery {
|
|
30
|
+
readonly tenantId: string;
|
|
31
|
+
readonly type?: AuditEvent['type'];
|
|
32
|
+
readonly actorId?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The scope query plus the page window (offset + limit) for {@link PagedAuditReader.read}. Mirrors
|
|
36
|
+
* the concrete `MariaDbAuditReader.read` param shape (all window fields optional) so the concrete
|
|
37
|
+
* reader is structurally assignable to {@link PagedAuditReader}; the service always supplies both.
|
|
38
|
+
*/
|
|
39
|
+
export interface PagedReadQuery extends CountQuery {
|
|
40
|
+
readonly limit?: number;
|
|
41
|
+
readonly offset?: number;
|
|
42
|
+
readonly before?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The numbered-pagination read port used by {@link AuditReadService.listPage}: an offset-windowed
|
|
46
|
+
* `read` and a scope-matched `count`. Defined LOCALLY here (not in `@astram/core`, which is
|
|
47
|
+
* frozen); the concrete `MariaDbAuditReader` is structurally assignable to it. NOT declared as
|
|
48
|
+
* `extends AuditReader` because its `read` widens the query shape (offset), which the frozen
|
|
49
|
+
* core `AuditReadQuery` does not carry.
|
|
50
|
+
*/
|
|
51
|
+
export interface PagedAuditReader {
|
|
52
|
+
read(query: PagedReadQuery): Promise<readonly AuditEvent[]>;
|
|
53
|
+
count(query: CountQuery): Promise<number>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The audit viewer read-model wrapper (design §2, §3): derives scope from the PRINCIPAL
|
|
57
|
+
* (never client input), reverse-resolves an actor name to an id, then serves a NUMBERED
|
|
58
|
+
* (offset + total) page — a scope-matched `count` for the total and an offset-windowed `read`
|
|
59
|
+
* for the rows — resolving actor/tenant labels DIRECTLY and stripping every raw id so the
|
|
60
|
+
* response carries labels only (ADR-0012).
|
|
61
|
+
*/
|
|
62
|
+
export declare class AuditReadService {
|
|
63
|
+
private readonly reader;
|
|
64
|
+
private readonly labels;
|
|
65
|
+
private readonly actorNameResolver;
|
|
66
|
+
constructor(reader: PagedAuditReader, labels: LabelResolver, actorNameResolver: ActorNameResolver);
|
|
67
|
+
/**
|
|
68
|
+
* A numbered (offset + total) audit page. Scope is derived from the PRINCIPAL ALONE — a
|
|
69
|
+
* non-root is pinned to its own tenant (tenantFilter ignored); a root gets the all-tenants
|
|
70
|
+
* sentinel, OPTIONALLY narrowed by tenantFilter. That SAME scope feeds BOTH the count and the
|
|
71
|
+
* page read, so the total can never leak another tenant's row count (invariant #4). A
|
|
72
|
+
* reader/count/resolver throw PROPAGATES (fail-closed) — we do not catch.
|
|
73
|
+
*/
|
|
74
|
+
listPage(principal: AuditPrincipal, filters: {
|
|
75
|
+
page?: number;
|
|
76
|
+
limit?: number;
|
|
77
|
+
type?: string;
|
|
78
|
+
actorName?: string;
|
|
79
|
+
tenantFilter?: string;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
rows: AuditRow[];
|
|
82
|
+
total: number;
|
|
83
|
+
page: number;
|
|
84
|
+
limit: number;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Remove obvious raw-uuid-bearing keys from the attributes detail so no raw id leaks.
|
|
88
|
+
* Keeps non-id fields; drops keys ending in `Uuid`/`Id`/`_uuid`/`_id` and any uuid-shaped value.
|
|
89
|
+
*/
|
|
90
|
+
private stripIds;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=audit-read.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-read.service.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit-read.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,wFAAwF;AACxF,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3C;AAMD;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;gBAFjB,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,aAAa,EACrB,iBAAiB,EAAE,iBAAiB;IAGvD;;;;;;OAMG;IACG,QAAQ,CACZ,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GACnG,OAAO,CAAC;QAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAyD5E;;;OAGG;IACH,OAAO,CAAC,QAAQ;CAUjB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuditReadService = void 0;
|
|
4
|
+
const ALL_TENANTS = '*';
|
|
5
|
+
const DEFAULT_LIMIT = 50;
|
|
6
|
+
const MAX_LIMIT = 200;
|
|
7
|
+
/**
|
|
8
|
+
* The audit viewer read-model wrapper (design §2, §3): derives scope from the PRINCIPAL
|
|
9
|
+
* (never client input), reverse-resolves an actor name to an id, then serves a NUMBERED
|
|
10
|
+
* (offset + total) page — a scope-matched `count` for the total and an offset-windowed `read`
|
|
11
|
+
* for the rows — resolving actor/tenant labels DIRECTLY and stripping every raw id so the
|
|
12
|
+
* response carries labels only (ADR-0012).
|
|
13
|
+
*/
|
|
14
|
+
class AuditReadService {
|
|
15
|
+
reader;
|
|
16
|
+
labels;
|
|
17
|
+
actorNameResolver;
|
|
18
|
+
constructor(reader, labels, actorNameResolver) {
|
|
19
|
+
this.reader = reader;
|
|
20
|
+
this.labels = labels;
|
|
21
|
+
this.actorNameResolver = actorNameResolver;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A numbered (offset + total) audit page. Scope is derived from the PRINCIPAL ALONE — a
|
|
25
|
+
* non-root is pinned to its own tenant (tenantFilter ignored); a root gets the all-tenants
|
|
26
|
+
* sentinel, OPTIONALLY narrowed by tenantFilter. That SAME scope feeds BOTH the count and the
|
|
27
|
+
* page read, so the total can never leak another tenant's row count (invariant #4). A
|
|
28
|
+
* reader/count/resolver throw PROPAGATES (fail-closed) — we do not catch.
|
|
29
|
+
*/
|
|
30
|
+
async listPage(principal, filters) {
|
|
31
|
+
// Scope is derived from the principal ALONE. A root gets the all-tenants sentinel,
|
|
32
|
+
// OPTIONALLY narrowed to one tenant via tenantFilter; an admin is pinned to their tenant
|
|
33
|
+
// and tenantFilter is ignored (cannot widen or cross).
|
|
34
|
+
let tenantId;
|
|
35
|
+
if (principal.isRoot) {
|
|
36
|
+
tenantId = filters.tenantFilter && filters.tenantFilter.length > 0 ? filters.tenantFilter : ALL_TENANTS;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
tenantId = principal.tenantId;
|
|
40
|
+
}
|
|
41
|
+
// Actor name -> id (server-side; uses the al_actor_id index). An unknown name yields an
|
|
42
|
+
// id that matches nothing → an empty page (correct, not an error).
|
|
43
|
+
let actorId;
|
|
44
|
+
if (filters.actorName && filters.actorName.length > 0) {
|
|
45
|
+
actorId = (await this.actorNameResolver(filters.actorName)) ?? ' no-such-actor';
|
|
46
|
+
}
|
|
47
|
+
// Clamp the client-supplied limit to [1, MAX_LIMIT]; a non-finite value falls back to
|
|
48
|
+
// DEFAULT_LIMIT (only an in-range-but-out-of-bounds number gets floored/capped). Floor the
|
|
49
|
+
// page to >= 1. offset uses the CLAMPED limit, so the window never diverges from read.limit.
|
|
50
|
+
const requestedLimit = Number.isFinite(filters.limit) ? filters.limit : DEFAULT_LIMIT;
|
|
51
|
+
const limit = Math.min(Math.max(1, requestedLimit), MAX_LIMIT);
|
|
52
|
+
const page = Number.isFinite(filters.page) && filters.page >= 1 ? Math.floor(filters.page) : 1;
|
|
53
|
+
const offset = (page - 1) * limit;
|
|
54
|
+
// ONE scope query — spread into BOTH the count and the page read so their tenant/type/actor
|
|
55
|
+
// WHERE is identical. A non-root's total therefore counts ONLY its own tenant (invariant #4).
|
|
56
|
+
const scopeQuery = {
|
|
57
|
+
tenantId,
|
|
58
|
+
...(filters.type ? { type: filters.type } : {}),
|
|
59
|
+
...(actorId ? { actorId } : {}),
|
|
60
|
+
};
|
|
61
|
+
// A throw in EITHER side propagates (fail-closed — a total must never silently be zero).
|
|
62
|
+
const [total, events] = await Promise.all([
|
|
63
|
+
this.reader.count(scopeQuery),
|
|
64
|
+
this.reader.read({ ...scopeQuery, limit, offset }),
|
|
65
|
+
]);
|
|
66
|
+
// Resolve labels DIRECTLY (the core `auditView` engine is no longer in this path): actorLabel
|
|
67
|
+
// via labels.resolve('principal', ...) and tenantLabel via labels.resolve('tenant', ...) —
|
|
68
|
+
// same resolver, same no-raw-id guarantee. A resolver throw propagates (fail-closed).
|
|
69
|
+
const rows = await Promise.all(events.map(async (event) => ({
|
|
70
|
+
occurredAt: event.occurredAtMs,
|
|
71
|
+
actorLabel: await this.labels.resolve('principal', event.actorId),
|
|
72
|
+
action: event.action?.key
|
|
73
|
+
?? event.action
|
|
74
|
+
?? '(event)',
|
|
75
|
+
tenantLabel: await this.labels.resolve('tenant', event.tenantId),
|
|
76
|
+
detail: this.stripIds(event.attributes ?? {}),
|
|
77
|
+
})));
|
|
78
|
+
return { rows, total, page, limit };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Remove obvious raw-uuid-bearing keys from the attributes detail so no raw id leaks.
|
|
82
|
+
* Keeps non-id fields; drops keys ending in `Uuid`/`Id`/`_uuid`/`_id` and any uuid-shaped value.
|
|
83
|
+
*/
|
|
84
|
+
stripIds(attrs) {
|
|
85
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const [k, val] of Object.entries(attrs)) {
|
|
88
|
+
if (/uuid$|Id$|_id$/i.test(k))
|
|
89
|
+
continue;
|
|
90
|
+
if (typeof val === 'string' && UUID.test(val))
|
|
91
|
+
continue;
|
|
92
|
+
out[k] = val;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.AuditReadService = AuditReadService;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Principal } from '@astram/engine';
|
|
2
|
+
import { AuditReadService } from './audit-read.service.js';
|
|
3
|
+
/** The request shape after the enforcement guard attaches the resolved principal (mirrors
|
|
4
|
+
* grant-admin.controller.ts's `AuthedRequest`). `isRoot` is read off the same
|
|
5
|
+
* principal grant-admin uses via `req.principal?.id` — kept optional here for the same
|
|
6
|
+
* reason grant-admin reads `req.principal?.id` defensively. */
|
|
7
|
+
interface AuthedRequest {
|
|
8
|
+
principal: Principal & {
|
|
9
|
+
isRoot?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `GET /identity/audit` — the audit viewer read endpoint (design §4). Scope is derived
|
|
14
|
+
* from the request principal by the service; the controller NEVER trusts a client-supplied
|
|
15
|
+
* tenant/scope. A service throw (reader/resolver outage) → 503 (fail-closed).
|
|
16
|
+
*/
|
|
17
|
+
export declare class AuditController {
|
|
18
|
+
private readonly service;
|
|
19
|
+
constructor(service: AuditReadService);
|
|
20
|
+
list(req: AuthedRequest, q: {
|
|
21
|
+
page?: string;
|
|
22
|
+
limit?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
actor?: string;
|
|
25
|
+
tenant?: string;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
success: true;
|
|
28
|
+
data: unknown[];
|
|
29
|
+
total: number;
|
|
30
|
+
page: number;
|
|
31
|
+
limit: number;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=audit.controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.controller.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit.controller.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAIhD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAW3D;;;gEAGgE;AAChE,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,GAAG;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC7C;AAED;;;;GAIG;AACH,qBACa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,gBAAgB;IAIhD,IAAI,CACD,GAAG,EAAE,aAAa,EAChB,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5F,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CA8B3F"}
|
|
@@ -0,0 +1,73 @@
|
|
|
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.AuditController = void 0;
|
|
16
|
+
// audit.controller.ts
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const guard_1 = require("@astram/guard");
|
|
19
|
+
// `AuditReadService` is the DI token for the constructor injection below, so it MUST be a
|
|
20
|
+
// value import — a type-only import is erased at compile time, leaving NestJS with
|
|
21
|
+
// `undefined` metadata and an "can't resolve dependencies" boot failure.
|
|
22
|
+
const audit_read_service_js_1 = require("./audit-read.service.js");
|
|
23
|
+
/**
|
|
24
|
+
* `GET /identity/audit` — the audit viewer read endpoint (design §4). Scope is derived
|
|
25
|
+
* from the request principal by the service; the controller NEVER trusts a client-supplied
|
|
26
|
+
* tenant/scope. A service throw (reader/resolver outage) → 503 (fail-closed).
|
|
27
|
+
*/
|
|
28
|
+
let AuditController = class AuditController {
|
|
29
|
+
service;
|
|
30
|
+
constructor(service) {
|
|
31
|
+
this.service = service;
|
|
32
|
+
}
|
|
33
|
+
async list(req, q) {
|
|
34
|
+
const principal = {
|
|
35
|
+
id: req.principal.id,
|
|
36
|
+
tenantId: req.principal.tenantId,
|
|
37
|
+
isRoot: req.principal.isRoot === true,
|
|
38
|
+
};
|
|
39
|
+
// `page`/`limit` are passed through ONLY when present AND finite — otherwise the key is
|
|
40
|
+
// OMITTED so `listPage` applies its own default + clamp/floor. A non-numeric value (e.g.
|
|
41
|
+
// `?page=abc`) parses to `NaN`, which is non-finite, so it is omitted rather than forwarded.
|
|
42
|
+
const page = Number(q.page);
|
|
43
|
+
const limit = Number(q.limit);
|
|
44
|
+
const filters = {
|
|
45
|
+
...(q.page !== undefined && Number.isFinite(page) ? { page } : {}),
|
|
46
|
+
...(q.limit !== undefined && Number.isFinite(limit) ? { limit } : {}),
|
|
47
|
+
...(q.type ? { type: q.type } : {}),
|
|
48
|
+
...(q.actor ? { actorName: q.actor } : {}),
|
|
49
|
+
...(q.tenant ? { tenantFilter: q.tenant } : {}),
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const { rows, total, page: resolvedPage, limit: resolvedLimit, } = await this.service.listPage(principal, filters);
|
|
53
|
+
return { success: true, data: rows, total, page: resolvedPage, limit: resolvedLimit };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
throw new common_1.ServiceUnavailableException(`audit read unavailable: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
exports.AuditController = AuditController;
|
|
61
|
+
__decorate([
|
|
62
|
+
(0, common_1.Get)('audit'),
|
|
63
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
64
|
+
__param(0, (0, common_1.Req)()),
|
|
65
|
+
__param(1, (0, common_1.Query)()),
|
|
66
|
+
__metadata("design:type", Function),
|
|
67
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
68
|
+
__metadata("design:returntype", Promise)
|
|
69
|
+
], AuditController.prototype, "list", null);
|
|
70
|
+
exports.AuditController = AuditController = __decorate([
|
|
71
|
+
(0, common_1.Controller)('identity'),
|
|
72
|
+
__metadata("design:paramtypes", [audit_read_service_js_1.AuditReadService])
|
|
73
|
+
], AuditController);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LabelResolver, LabelKind } from '@astram/core';
|
|
2
|
+
import type { AuditQueryable } from './mariadb-audit-reader.js';
|
|
3
|
+
/**
|
|
4
|
+
* The id->human-label chokepoint (ADR-0012, design §2.2). A deleted/unknown
|
|
5
|
+
* entity resolves to a FALLBACK label (never the id); a directory OUTAGE throws
|
|
6
|
+
* (fail-closed — a transient must not masquerade as a deletion).
|
|
7
|
+
*/
|
|
8
|
+
export declare class DirectoryLabelResolver implements LabelResolver {
|
|
9
|
+
private readonly db;
|
|
10
|
+
constructor(db: AuditQueryable);
|
|
11
|
+
resolve(kind: LabelKind, id: string, _tenantId?: string): Promise<string>;
|
|
12
|
+
private fallback;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=directory-label-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directory-label-resolver.d.ts","sourceRoot":"","sources":["../../../src/features/audit/directory-label-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAIhE;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,aAAa;IAC9C,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,cAAc;IAEzC,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA8B/E,OAAO,CAAC,QAAQ;CAMjB"}
|