@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,85 @@
|
|
|
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.CatalogBootstrapProvider = void 0;
|
|
16
|
+
// Ported from astram3 `server/src/features/authz/catalog-bootstrap.provider.ts`.
|
|
17
|
+
// Faithful port; imports repointed to the framework's guard/engine + local jobs. The
|
|
18
|
+
// astram3 `./module-registry.bridge` import is repointed to `buildIdentityModuleRegistry`
|
|
19
|
+
// exported from @astram/guard.
|
|
20
|
+
const common_1 = require("@nestjs/common");
|
|
21
|
+
const core_1 = require("@nestjs/core");
|
|
22
|
+
const guard_1 = require("@astram/guard");
|
|
23
|
+
const engine_1 = require("@astram/engine");
|
|
24
|
+
const catalog_reconcile_job_js_1 = require("./catalog-reconcile.job.js");
|
|
25
|
+
const access_bundle_reconcile_job_js_1 = require("./access-bundle-reconcile.job.js");
|
|
26
|
+
const MODULE_KEY = 'identity';
|
|
27
|
+
const RECONCILE_INTERVAL_MS = 60 * 60 * 1000; // hourly; honors DISABLE_ALL_CRONS below
|
|
28
|
+
/**
|
|
29
|
+
* Boot: read-only verifyGatable (fail-closed — a stranded/unclassified route aborts
|
|
30
|
+
* boot). Post-boot: kick the mutating reconcile once, then on an interval (single
|
|
31
|
+
* process = single runner in dev; a clustered deploy would gate this behind a leader
|
|
32
|
+
* lock — deferred). DISABLE_ALL_CRONS disables the interval (boot verify still runs).
|
|
33
|
+
*
|
|
34
|
+
* The mutating half (the `CatalogReconcileJob`, holding the latest report for
|
|
35
|
+
* /health/catalog) is supplied by DI via the module factory — the SAME instance the
|
|
36
|
+
* health controller injects — so verify, the post-boot pass, and the endpoint all share
|
|
37
|
+
* one job. This provider owns only the boot verify + the schedule.
|
|
38
|
+
*/
|
|
39
|
+
let CatalogBootstrapProvider = class CatalogBootstrapProvider {
|
|
40
|
+
discovery;
|
|
41
|
+
scanner;
|
|
42
|
+
reflector;
|
|
43
|
+
job;
|
|
44
|
+
accessBundleJob;
|
|
45
|
+
registrations;
|
|
46
|
+
timer = null;
|
|
47
|
+
constructor(discovery, scanner, reflector, job, accessBundleJob, registrations) {
|
|
48
|
+
this.discovery = discovery;
|
|
49
|
+
this.scanner = scanner;
|
|
50
|
+
this.reflector = reflector;
|
|
51
|
+
this.job = job;
|
|
52
|
+
this.accessBundleJob = accessBundleJob;
|
|
53
|
+
this.registrations = registrations;
|
|
54
|
+
}
|
|
55
|
+
async onApplicationBootstrap() {
|
|
56
|
+
const registry = (0, guard_1.buildIdentityModuleRegistry)(this.registrations);
|
|
57
|
+
const inventoryScanner = new guard_1.OperationInventoryScanner(this.discovery, this.scanner, this.reflector);
|
|
58
|
+
const inventory = inventoryScanner.scan(MODULE_KEY);
|
|
59
|
+
// Fail-closed (ADR-0002): a route that can't be gated aborts boot.
|
|
60
|
+
new guard_1.CatalogReconciler().verifyGatable(inventory, registry);
|
|
61
|
+
// Mutating reconcile runs AFTER boot (not blocking, not fail-closed).
|
|
62
|
+
const nowIso = new Date().toISOString();
|
|
63
|
+
await this.job.runOnce(nowIso);
|
|
64
|
+
// Bundles reconcile AFTER permissions (same post-boot pass): a bundle validates
|
|
65
|
+
// its keys against the live permission catalog, so permissions must reconcile first.
|
|
66
|
+
await this.accessBundleJob.runOnce(nowIso);
|
|
67
|
+
if (process.env.DISABLE_ALL_CRONS !== 'true') {
|
|
68
|
+
this.timer = setInterval(() => {
|
|
69
|
+
const tickIso = new Date().toISOString();
|
|
70
|
+
void this.job.runOnce(tickIso).then(() => this.accessBundleJob.runOnce(tickIso));
|
|
71
|
+
}, RECONCILE_INTERVAL_MS);
|
|
72
|
+
this.timer.unref?.();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
exports.CatalogBootstrapProvider = CatalogBootstrapProvider;
|
|
77
|
+
exports.CatalogBootstrapProvider = CatalogBootstrapProvider = __decorate([
|
|
78
|
+
(0, common_1.Injectable)(),
|
|
79
|
+
__param(5, (0, common_1.Inject)(engine_1.ENGINE_ALL_REGISTRATIONS)),
|
|
80
|
+
__metadata("design:paramtypes", [core_1.DiscoveryService,
|
|
81
|
+
core_1.MetadataScanner,
|
|
82
|
+
core_1.Reflector,
|
|
83
|
+
catalog_reconcile_job_js_1.CatalogReconcileJob,
|
|
84
|
+
access_bundle_reconcile_job_js_1.AccessBundleReconcileJob, Array])
|
|
85
|
+
], CatalogBootstrapProvider);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Pool } from 'mariadb';
|
|
2
|
+
import { type ReconcileReport } from '@astram/guard';
|
|
3
|
+
import type { ModuleRegistry } from '@astram/core';
|
|
4
|
+
/**
|
|
5
|
+
* Single-runner wrapper around the framework CatalogReconciler.reconcile (the mutating
|
|
6
|
+
* half). Called once shortly after boot and on a cron. Holds the latest report for
|
|
7
|
+
* /health/catalog. NOT run at boot (boot only does the read-only verifyGatable).
|
|
8
|
+
*
|
|
9
|
+
* Timestamp is supplied by the caller (policy: pass new Date().toISOString() from the
|
|
10
|
+
* scheduler, not inside the reconciler — keeps the framework fn pure).
|
|
11
|
+
*/
|
|
12
|
+
export declare class CatalogReconcileJob {
|
|
13
|
+
private readonly registry;
|
|
14
|
+
private readonly reconciler;
|
|
15
|
+
private readonly store;
|
|
16
|
+
private latest;
|
|
17
|
+
private running;
|
|
18
|
+
constructor(pool: Pool, registry: ModuleRegistry);
|
|
19
|
+
/** Idempotent single pass. Guarded so overlapping ticks don't double-run. */
|
|
20
|
+
runOnce(nowIso: string): Promise<ReconcileReport | null>;
|
|
21
|
+
getLatest(): ReconcileReport | null;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=catalog-reconcile.job.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-reconcile.job.d.ts","sourceRoot":"","sources":["../../../src/features/authz/catalog-reconcile.job.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD;;;;;;;GAOG;AACH,qBAAa,mBAAmB;IAMN,OAAO,CAAC,QAAQ,CAAC,QAAQ;IALjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,IAAI,EAAmB,QAAQ,EAAE,cAAc;IAIjE,6EAA6E;IACvE,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAmB9D,SAAS,IAAI,eAAe,GAAG,IAAI;CAGpC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CatalogReconcileJob = void 0;
|
|
4
|
+
const guard_1 = require("@astram/guard");
|
|
5
|
+
const mariadb_catalog_store_js_1 = require("./mariadb-catalog-store.js");
|
|
6
|
+
/**
|
|
7
|
+
* Single-runner wrapper around the framework CatalogReconciler.reconcile (the mutating
|
|
8
|
+
* half). Called once shortly after boot and on a cron. Holds the latest report for
|
|
9
|
+
* /health/catalog. NOT run at boot (boot only does the read-only verifyGatable).
|
|
10
|
+
*
|
|
11
|
+
* Timestamp is supplied by the caller (policy: pass new Date().toISOString() from the
|
|
12
|
+
* scheduler, not inside the reconciler — keeps the framework fn pure).
|
|
13
|
+
*/
|
|
14
|
+
class CatalogReconcileJob {
|
|
15
|
+
registry;
|
|
16
|
+
reconciler = new guard_1.CatalogReconciler();
|
|
17
|
+
store;
|
|
18
|
+
latest = null;
|
|
19
|
+
running = false;
|
|
20
|
+
constructor(pool, registry) {
|
|
21
|
+
this.registry = registry;
|
|
22
|
+
this.store = new mariadb_catalog_store_js_1.MariaDbCatalogStore(pool);
|
|
23
|
+
}
|
|
24
|
+
/** Idempotent single pass. Guarded so overlapping ticks don't double-run. */
|
|
25
|
+
async runOnce(nowIso) {
|
|
26
|
+
if (this.running)
|
|
27
|
+
return this.latest;
|
|
28
|
+
this.running = true;
|
|
29
|
+
try {
|
|
30
|
+
const report = await this.reconciler.reconcile(this.registry, this.store, nowIso);
|
|
31
|
+
this.latest = report;
|
|
32
|
+
const orphanNote = report.orphaned.length
|
|
33
|
+
? ` — ORPHANED: ${report.orphaned.join(', ')}`
|
|
34
|
+
: '';
|
|
35
|
+
console.log(`[catalog] reconcile: +${report.added.length} added, ${report.orphaned.length} orphaned, ` +
|
|
36
|
+
`${report.reactivated.length} reactivated${orphanNote}`);
|
|
37
|
+
return report;
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
this.running = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
getLatest() {
|
|
44
|
+
return this.latest;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.CatalogReconcileJob = CatalogReconcileJob;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionFactsPort adapter — resolves a principal's coarse grants (the set of exact
|
|
3
|
+
* permission keys it holds via its roles) for core's object-type gate.
|
|
4
|
+
*
|
|
5
|
+
* roleIds → role_permission rows (exact OR wildcard strings) → expand wildcards against the
|
|
6
|
+
* registry's real action universe → de-duplicated exact keys. Core then does exact `.includes`.
|
|
7
|
+
*
|
|
8
|
+
* FAIL-CLOSED (ADR-0016): a store OUTAGE throws AuthorizationUnavailableError (→ 503).
|
|
9
|
+
* Data-absence ("no roles"/"no grants") returns EMPTY keys — a legitimate deny input.
|
|
10
|
+
* NEVER return empty to mask an outage; NEVER fabricate a key.
|
|
11
|
+
*
|
|
12
|
+
* Ported from astram3 `server/src/features/authz/permission-facts.adapter.ts`.
|
|
13
|
+
*/
|
|
14
|
+
import type { Pool } from 'mariadb';
|
|
15
|
+
import type { PermissionFactsPort, CoarseGrants, ModuleRegistry } from '@astram/core';
|
|
16
|
+
/**
|
|
17
|
+
* Expand raw grant strings (exact or wildcard) to the set of EXACT registered keys.
|
|
18
|
+
* Bounded to `allKeys` — a wildcard can never yield a key that isn't registered.
|
|
19
|
+
* exact `identity:company:view` → itself (if registered)
|
|
20
|
+
* `identity:company:*` → every registered `identity:company:<action>`
|
|
21
|
+
* `identity:*` / bare `identity` → every registered `identity:<type>:<action>`
|
|
22
|
+
*/
|
|
23
|
+
export declare function expandGrantKeys(rawKeys: string[], allKeys: string[]): string[];
|
|
24
|
+
/**
|
|
25
|
+
* The principal's role uuids from its ACTIVE membership's in-effect postings in
|
|
26
|
+
* `tenantId` — the SAME in-effect-posting window query `DbPermissionFactsAdapter` uses,
|
|
27
|
+
* extracted so any OTHER caller that needs a principal's roleIds for a tenant reuses this
|
|
28
|
+
* ONE timezone-safe epoch-ms window read instead of re-inlining the SQL.
|
|
29
|
+
*
|
|
30
|
+
* @param now injectable clock (epoch-ms), defaults to `Date.now` — bound as a `?`
|
|
31
|
+
* integer against the BIGINT `mp_effective_*_ms` columns (timezone-immune).
|
|
32
|
+
*/
|
|
33
|
+
export declare function readRoleIdsForTenant(pool: Pool, principalId: string, tenantId: string, now?: () => number): Promise<string[]>;
|
|
34
|
+
export declare class DbPermissionFactsAdapter implements PermissionFactsPort {
|
|
35
|
+
private readonly pool;
|
|
36
|
+
private readonly registry;
|
|
37
|
+
/**
|
|
38
|
+
* Injectable clock (epoch-ms) for the posting effective-window compare. Defaults to
|
|
39
|
+
* `Date.now`. Bound as a `?` integer against the BIGINT `_ms` columns — timezone-immune.
|
|
40
|
+
* Injectable so a test can fix the clock relative to a seeded expired/active posting.
|
|
41
|
+
*/
|
|
42
|
+
private readonly now;
|
|
43
|
+
constructor(pool: Pool, registry: ModuleRegistry,
|
|
44
|
+
/**
|
|
45
|
+
* Injectable clock (epoch-ms) for the posting effective-window compare. Defaults to
|
|
46
|
+
* `Date.now`. Bound as a `?` integer against the BIGINT `_ms` columns — timezone-immune.
|
|
47
|
+
* Injectable so a test can fix the clock relative to a seeded expired/active posting.
|
|
48
|
+
*/
|
|
49
|
+
now?: () => number);
|
|
50
|
+
currentPermissionsVersion(_principalId: string, tenantId: string): Promise<number>;
|
|
51
|
+
loadCoarseGrants(principalId: string, tenantId: string): Promise<CoarseGrants>;
|
|
52
|
+
private readVersion;
|
|
53
|
+
/**
|
|
54
|
+
* The principal's role uuids from its ACTIVE membership's in-effect postings in this
|
|
55
|
+
* tenant — delegates to the shared {@link readRoleIdsForTenant} (single source).
|
|
56
|
+
*/
|
|
57
|
+
private readRoleIds;
|
|
58
|
+
private readGrantKeys;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=db-permission-facts.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-permission-facts.adapter.d.ts","sourceRoot":"","sources":["../../../src/features/authz/db-permission-facts.adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKtF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAe9E;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB,GAC3B,OAAO,CAAC,MAAM,EAAE,CAAC,CAYnB;AAED,qBAAa,wBAAyB,YAAW,mBAAmB;IAEhE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAPH,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,cAAc;IACzC;;;;OAIG;IACc,GAAG,GAAE,MAAM,MAAiB;IAGzC,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAalF,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YAgBtE,WAAW;IASzB;;;OAGG;YACW,WAAW;YAIX,aAAa;CAS5B"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbPermissionFactsAdapter = void 0;
|
|
4
|
+
exports.expandGrantKeys = expandGrantKeys;
|
|
5
|
+
exports.readRoleIdsForTenant = readRoleIdsForTenant;
|
|
6
|
+
const core_1 = require("@astram/core");
|
|
7
|
+
const MODULE_PREFIX = 'identity';
|
|
8
|
+
/**
|
|
9
|
+
* Expand raw grant strings (exact or wildcard) to the set of EXACT registered keys.
|
|
10
|
+
* Bounded to `allKeys` — a wildcard can never yield a key that isn't registered.
|
|
11
|
+
* exact `identity:company:view` → itself (if registered)
|
|
12
|
+
* `identity:company:*` → every registered `identity:company:<action>`
|
|
13
|
+
* `identity:*` / bare `identity` → every registered `identity:<type>:<action>`
|
|
14
|
+
*/
|
|
15
|
+
function expandGrantKeys(rawKeys, allKeys) {
|
|
16
|
+
const out = new Set();
|
|
17
|
+
for (const raw of rawKeys) {
|
|
18
|
+
if (raw === MODULE_PREFIX || raw === `${MODULE_PREFIX}:*`) {
|
|
19
|
+
for (const k of allKeys)
|
|
20
|
+
if (k.startsWith(`${MODULE_PREFIX}:`))
|
|
21
|
+
out.add(k);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (raw.endsWith(':*')) {
|
|
25
|
+
const prefix = raw.slice(0, -1); // keep trailing ':' → 'identity:company:'
|
|
26
|
+
for (const k of allKeys)
|
|
27
|
+
if (k.startsWith(prefix))
|
|
28
|
+
out.add(k);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (allKeys.includes(raw))
|
|
32
|
+
out.add(raw); // exact, only if real
|
|
33
|
+
}
|
|
34
|
+
return [...out];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The principal's role uuids from its ACTIVE membership's in-effect postings in
|
|
38
|
+
* `tenantId` — the SAME in-effect-posting window query `DbPermissionFactsAdapter` uses,
|
|
39
|
+
* extracted so any OTHER caller that needs a principal's roleIds for a tenant reuses this
|
|
40
|
+
* ONE timezone-safe epoch-ms window read instead of re-inlining the SQL.
|
|
41
|
+
*
|
|
42
|
+
* @param now injectable clock (epoch-ms), defaults to `Date.now` — bound as a `?`
|
|
43
|
+
* integer against the BIGINT `mp_effective_*_ms` columns (timezone-immune).
|
|
44
|
+
*/
|
|
45
|
+
async function readRoleIdsForTenant(pool, principalId, tenantId, now = Date.now) {
|
|
46
|
+
const nowMs = now();
|
|
47
|
+
const rows = (await pool.query(`SELECT DISTINCT mp.mp_role_uuid AS role_uuid
|
|
48
|
+
FROM \`shield_app\`.\`company_membership\` cm
|
|
49
|
+
JOIN \`shield_app\`.\`membership_posting\` mp ON mp.mp_membership_uuid = cm.cm_uuid
|
|
50
|
+
WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ? AND cm.cm_is_active = 1
|
|
51
|
+
AND (mp.mp_effective_from_ms IS NULL OR mp.mp_effective_from_ms <= ?)
|
|
52
|
+
AND (mp.mp_effective_until_ms IS NULL OR mp.mp_effective_until_ms > ?)`, [principalId, tenantId, nowMs, nowMs]));
|
|
53
|
+
return rows.map((r) => r.role_uuid);
|
|
54
|
+
}
|
|
55
|
+
class DbPermissionFactsAdapter {
|
|
56
|
+
pool;
|
|
57
|
+
registry;
|
|
58
|
+
now;
|
|
59
|
+
constructor(pool, registry,
|
|
60
|
+
/**
|
|
61
|
+
* Injectable clock (epoch-ms) for the posting effective-window compare. Defaults to
|
|
62
|
+
* `Date.now`. Bound as a `?` integer against the BIGINT `_ms` columns — timezone-immune.
|
|
63
|
+
* Injectable so a test can fix the clock relative to a seeded expired/active posting.
|
|
64
|
+
*/
|
|
65
|
+
now = Date.now) {
|
|
66
|
+
this.pool = pool;
|
|
67
|
+
this.registry = registry;
|
|
68
|
+
this.now = now;
|
|
69
|
+
}
|
|
70
|
+
async currentPermissionsVersion(_principalId, tenantId) {
|
|
71
|
+
try {
|
|
72
|
+
const rows = (await this.pool.query(`SELECT apv_version FROM \`shield_authz\`.\`authz_permission_version\`
|
|
73
|
+
WHERE apv_tenant_uuid = ? LIMIT 1`, [tenantId]));
|
|
74
|
+
return rows.length > 0 ? Number(rows[0]?.apv_version) : 0;
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
throw new core_1.AuthorizationUnavailableError('permission version store unavailable', cause);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async loadCoarseGrants(principalId, tenantId) {
|
|
81
|
+
let version;
|
|
82
|
+
let roleIds;
|
|
83
|
+
let rawKeys;
|
|
84
|
+
try {
|
|
85
|
+
version = await this.readVersion(tenantId);
|
|
86
|
+
roleIds = await this.readRoleIds(principalId, tenantId);
|
|
87
|
+
rawKeys = roleIds.length === 0 ? [] : await this.readGrantKeys(roleIds);
|
|
88
|
+
}
|
|
89
|
+
catch (cause) {
|
|
90
|
+
if (cause instanceof core_1.AuthorizationUnavailableError)
|
|
91
|
+
throw cause;
|
|
92
|
+
throw new core_1.AuthorizationUnavailableError('permission facts store unavailable', cause);
|
|
93
|
+
}
|
|
94
|
+
const allKeys = this.registry.listActions().map((a) => a.permissionKey);
|
|
95
|
+
return { permissionsVersion: version, grantedPermissionKeys: expandGrantKeys(rawKeys, allKeys) };
|
|
96
|
+
}
|
|
97
|
+
async readVersion(tenantId) {
|
|
98
|
+
const rows = (await this.pool.query(`SELECT apv_version FROM \`shield_authz\`.\`authz_permission_version\`
|
|
99
|
+
WHERE apv_tenant_uuid = ? LIMIT 1`, [tenantId]));
|
|
100
|
+
return rows.length > 0 ? Number(rows[0]?.apv_version) : 0;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The principal's role uuids from its ACTIVE membership's in-effect postings in this
|
|
104
|
+
* tenant — delegates to the shared {@link readRoleIdsForTenant} (single source).
|
|
105
|
+
*/
|
|
106
|
+
async readRoleIds(principalId, tenantId) {
|
|
107
|
+
return readRoleIdsForTenant(this.pool, principalId, tenantId, this.now);
|
|
108
|
+
}
|
|
109
|
+
async readGrantKeys(roleIds) {
|
|
110
|
+
const placeholders = roleIds.map(() => '?').join(', ');
|
|
111
|
+
const rows = (await this.pool.query(`SELECT rp_perm_key FROM \`shield_authz\`.\`role_permission\`
|
|
112
|
+
WHERE rp_role_uuid IN (${placeholders})`, roleIds));
|
|
113
|
+
return rows.map((r) => r.rp_perm_key);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.DbPermissionFactsAdapter = DbPermissionFactsAdapter;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Principal } from '@astram/engine';
|
|
2
|
+
import { GrantAdminService } from './grant-admin.service.js';
|
|
3
|
+
/** The request shape after the enforcement guard attaches the resolved principal. */
|
|
4
|
+
interface AuthedRequest {
|
|
5
|
+
principal: Principal;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Grant administration — list/grant/revoke a role's permission keys. Gated purely on
|
|
9
|
+
* `@Classified({ typeKey: 'role', ... })` + `decide()` (G1: no handler-level root
|
|
10
|
+
* check). Managing grants IS role administration, so it keys to the `role` object type:
|
|
11
|
+
* read routes need `identity:role:read`, mutating routes need `identity:role:update`.
|
|
12
|
+
* `role` is a CONTROL-plane type, so the capped root is allowed without an explicit
|
|
13
|
+
* grant; a non-root caller needs the grant. (`role_permission` itself is not a
|
|
14
|
+
* registered object type — it is administered THROUGH `role`, never classified directly.)
|
|
15
|
+
*
|
|
16
|
+
* Mutating routes pass `req.principal.id` as the grantor so `role_permission` records
|
|
17
|
+
* who granted what (surfaced by the grants-summary listing as "by <name>").
|
|
18
|
+
*/
|
|
19
|
+
export declare class GrantAdminController {
|
|
20
|
+
private readonly grants;
|
|
21
|
+
constructor(grants: GrantAdminService);
|
|
22
|
+
/**
|
|
23
|
+
* Per-role access summary for the Access listing: every role that holds ≥1 grant, with
|
|
24
|
+
* its object/permission counts, access chips, covering preset, and last-modified date +
|
|
25
|
+
* grantor NAME (resolved server-side — no raw uuid). Gated `role:read`.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ Path is `access/role-grants` (NOT under `roles/...`) on purpose: a `roles/<x>` path
|
|
28
|
+
* would be captured by IdentityController's `@Get('roles/:uuid')` (registered first),
|
|
29
|
+
* routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
|
|
30
|
+
*/
|
|
31
|
+
grantsSummary(query: {
|
|
32
|
+
page?: string;
|
|
33
|
+
limit?: string;
|
|
34
|
+
search?: string;
|
|
35
|
+
sortBy?: string;
|
|
36
|
+
sortOrder?: string;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
rows: import("./grant-admin.service.js").RoleGrantSummary[];
|
|
39
|
+
total: number;
|
|
40
|
+
page: number;
|
|
41
|
+
limit: number;
|
|
42
|
+
}>;
|
|
43
|
+
/** List all grantable permission keys + wildcards. */
|
|
44
|
+
catalog(): Promise<{
|
|
45
|
+
catalog: {
|
|
46
|
+
key: string;
|
|
47
|
+
label: string;
|
|
48
|
+
module: string;
|
|
49
|
+
typeKey: string;
|
|
50
|
+
action: string;
|
|
51
|
+
}[];
|
|
52
|
+
wildcards: string[];
|
|
53
|
+
}>;
|
|
54
|
+
/** List a role's current grants. */
|
|
55
|
+
list(uuid: string): Promise<{
|
|
56
|
+
grants: string[];
|
|
57
|
+
}>;
|
|
58
|
+
/** Grant one permission key to a role. */
|
|
59
|
+
grant(req: AuthedRequest, uuid: string, body: {
|
|
60
|
+
key?: string;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
success: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
/** Revoke one permission key from a role. `:key` is URL-encoded (contains ':'). */
|
|
65
|
+
revoke(uuid: string, key: string): Promise<{
|
|
66
|
+
success: boolean;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* List all active access bundles (named, admin-grantable key sets) + their resolved
|
|
70
|
+
* atomic keys, for the Access UI. Bundles are identified by `key` — no `ab_uuid` is
|
|
71
|
+
* ever exposed. Gated `role:read` (managing/reading grants is role administration).
|
|
72
|
+
*/
|
|
73
|
+
bundles(): Promise<{
|
|
74
|
+
bundles: import("./grant-admin.service.js").BundleView[];
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Grant an access bundle to a role — expands the bundle to its atomic keys in
|
|
78
|
+
* `role_permission` (idempotent, one transaction, one version bump). Body: `{ key }`
|
|
79
|
+
* (the bundle slug). Unknown bundle → 400. Gated `role:update`.
|
|
80
|
+
*/
|
|
81
|
+
grantBundle(req: AuthedRequest, uuid: string, body: {
|
|
82
|
+
key?: string;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
success: boolean;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Revoke an access bundle from a role — removes exactly the bundle's atomic keys from
|
|
88
|
+
* `role_permission` (see design §5.1 caveat re: shared keys). `:key` is the bundle slug.
|
|
89
|
+
* Unknown bundle → 400. Gated `role:update`.
|
|
90
|
+
*/
|
|
91
|
+
revokeBundle(uuid: string, key: string): Promise<{
|
|
92
|
+
success: boolean;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
export {};
|
|
96
|
+
//# sourceMappingURL=grant-admin.controller.d.ts.map
|
|
@@ -0,0 +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;AAE1E,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,qBACa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,iBAAiB;IAEtD;;;;;;;;OAQG;IAGG,aAAa,CAEjB,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,oCAAoC;IAG9B,IAAI,CAAgB,IAAI,EAAE,MAAM;;;IAItC,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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
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.GrantAdminController = void 0;
|
|
16
|
+
// Ported from astram3 `server/src/features/authz/grant-admin.controller.ts`.
|
|
17
|
+
// Faithful port: same routes, same gating, same error mapping. Imports repointed to the
|
|
18
|
+
// framework's guard/engine building blocks + the local grant-admin.service.
|
|
19
|
+
const common_1 = require("@nestjs/common");
|
|
20
|
+
const guard_1 = require("@astram/guard");
|
|
21
|
+
const grant_admin_service_js_1 = require("./grant-admin.service.js");
|
|
22
|
+
/**
|
|
23
|
+
* Grant administration — list/grant/revoke a role's permission keys. Gated purely on
|
|
24
|
+
* `@Classified({ typeKey: 'role', ... })` + `decide()` (G1: no handler-level root
|
|
25
|
+
* check). Managing grants IS role administration, so it keys to the `role` object type:
|
|
26
|
+
* read routes need `identity:role:read`, mutating routes need `identity:role:update`.
|
|
27
|
+
* `role` is a CONTROL-plane type, so the capped root is allowed without an explicit
|
|
28
|
+
* grant; a non-root caller needs the grant. (`role_permission` itself is not a
|
|
29
|
+
* registered object type — it is administered THROUGH `role`, never classified directly.)
|
|
30
|
+
*
|
|
31
|
+
* Mutating routes pass `req.principal.id` as the grantor so `role_permission` records
|
|
32
|
+
* who granted what (surfaced by the grants-summary listing as "by <name>").
|
|
33
|
+
*/
|
|
34
|
+
let GrantAdminController = class GrantAdminController {
|
|
35
|
+
grants;
|
|
36
|
+
constructor(grants) {
|
|
37
|
+
this.grants = grants;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Per-role access summary for the Access listing: every role that holds ≥1 grant, with
|
|
41
|
+
* its object/permission counts, access chips, covering preset, and last-modified date +
|
|
42
|
+
* grantor NAME (resolved server-side — no raw uuid). Gated `role:read`.
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ Path is `access/role-grants` (NOT under `roles/...`) on purpose: a `roles/<x>` path
|
|
45
|
+
* would be captured by IdentityController's `@Get('roles/:uuid')` (registered first),
|
|
46
|
+
* routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
|
|
47
|
+
*/
|
|
48
|
+
async grantsSummary(query) {
|
|
49
|
+
const page = Number.parseInt(query?.page ?? '', 10);
|
|
50
|
+
const limit = Number.parseInt(query?.limit ?? '', 10);
|
|
51
|
+
const params = {
|
|
52
|
+
sortOrder: query?.sortOrder === 'desc' ? 'desc' : 'asc',
|
|
53
|
+
};
|
|
54
|
+
if (Number.isFinite(page))
|
|
55
|
+
params.page = page;
|
|
56
|
+
if (Number.isFinite(limit))
|
|
57
|
+
params.limit = limit;
|
|
58
|
+
if (typeof query?.search === 'string')
|
|
59
|
+
params.search = query.search;
|
|
60
|
+
if (typeof query?.sortBy === 'string')
|
|
61
|
+
params.sortBy = query.sortBy;
|
|
62
|
+
return this.grants.listGrantsSummary(params);
|
|
63
|
+
}
|
|
64
|
+
/** List all grantable permission keys + wildcards. */
|
|
65
|
+
async catalog() {
|
|
66
|
+
return this.grants.listCatalog();
|
|
67
|
+
}
|
|
68
|
+
/** List a role's current grants. */
|
|
69
|
+
async list(uuid) {
|
|
70
|
+
return this.grants.listRoleGrants(uuid);
|
|
71
|
+
}
|
|
72
|
+
/** Grant one permission key to a role. */
|
|
73
|
+
async grant(req, uuid, body) {
|
|
74
|
+
const key = body?.key;
|
|
75
|
+
if (!key)
|
|
76
|
+
throw new common_1.BadRequestException('key required');
|
|
77
|
+
try {
|
|
78
|
+
await this.grants.grant(uuid, key, req.principal?.id);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
82
|
+
throw new common_1.BadRequestException(e.message);
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
return { success: true };
|
|
86
|
+
}
|
|
87
|
+
/** Revoke one permission key from a role. `:key` is URL-encoded (contains ':'). */
|
|
88
|
+
async revoke(uuid, key) {
|
|
89
|
+
await this.grants.revoke(uuid, decodeURIComponent(key));
|
|
90
|
+
return { success: true };
|
|
91
|
+
}
|
|
92
|
+
// ── Access bundles ────────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* List all active access bundles (named, admin-grantable key sets) + their resolved
|
|
95
|
+
* atomic keys, for the Access UI. Bundles are identified by `key` — no `ab_uuid` is
|
|
96
|
+
* ever exposed. Gated `role:read` (managing/reading grants is role administration).
|
|
97
|
+
*/
|
|
98
|
+
async bundles() {
|
|
99
|
+
return this.grants.listBundles();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Grant an access bundle to a role — expands the bundle to its atomic keys in
|
|
103
|
+
* `role_permission` (idempotent, one transaction, one version bump). Body: `{ key }`
|
|
104
|
+
* (the bundle slug). Unknown bundle → 400. Gated `role:update`.
|
|
105
|
+
*/
|
|
106
|
+
async grantBundle(req, uuid, body) {
|
|
107
|
+
const key = body?.key;
|
|
108
|
+
if (!key)
|
|
109
|
+
throw new common_1.BadRequestException('key required');
|
|
110
|
+
try {
|
|
111
|
+
await this.grants.grantBundle(uuid, key, req.principal?.id);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
115
|
+
throw new common_1.BadRequestException(e.message);
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
return { success: true };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Revoke an access bundle from a role — removes exactly the bundle's atomic keys from
|
|
122
|
+
* `role_permission` (see design §5.1 caveat re: shared keys). `:key` is the bundle slug.
|
|
123
|
+
* Unknown bundle → 400. Gated `role:update`.
|
|
124
|
+
*/
|
|
125
|
+
async revokeBundle(uuid, key) {
|
|
126
|
+
try {
|
|
127
|
+
await this.grants.revokeBundle(uuid, decodeURIComponent(key));
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
131
|
+
throw new common_1.BadRequestException(e.message);
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
return { success: true };
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
exports.GrantAdminController = GrantAdminController;
|
|
138
|
+
__decorate([
|
|
139
|
+
(0, common_1.Get)('access/role-grants'),
|
|
140
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
141
|
+
__param(0, (0, common_1.Query)()),
|
|
142
|
+
__metadata("design:type", Function),
|
|
143
|
+
__metadata("design:paramtypes", [Object]),
|
|
144
|
+
__metadata("design:returntype", Promise)
|
|
145
|
+
], GrantAdminController.prototype, "grantsSummary", null);
|
|
146
|
+
__decorate([
|
|
147
|
+
(0, common_1.Get)('permission-catalog'),
|
|
148
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
149
|
+
__metadata("design:type", Function),
|
|
150
|
+
__metadata("design:paramtypes", []),
|
|
151
|
+
__metadata("design:returntype", Promise)
|
|
152
|
+
], GrantAdminController.prototype, "catalog", null);
|
|
153
|
+
__decorate([
|
|
154
|
+
(0, common_1.Get)('roles/:uuid/permissions'),
|
|
155
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
156
|
+
__param(0, (0, common_1.Param)('uuid')),
|
|
157
|
+
__metadata("design:type", Function),
|
|
158
|
+
__metadata("design:paramtypes", [String]),
|
|
159
|
+
__metadata("design:returntype", Promise)
|
|
160
|
+
], GrantAdminController.prototype, "list", null);
|
|
161
|
+
__decorate([
|
|
162
|
+
(0, common_1.Post)('roles/:uuid/permissions'),
|
|
163
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
164
|
+
__param(0, (0, common_1.Req)()),
|
|
165
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
166
|
+
__param(2, (0, common_1.Body)()),
|
|
167
|
+
__metadata("design:type", Function),
|
|
168
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
169
|
+
__metadata("design:returntype", Promise)
|
|
170
|
+
], GrantAdminController.prototype, "grant", null);
|
|
171
|
+
__decorate([
|
|
172
|
+
(0, common_1.Delete)('roles/:uuid/permissions/:key'),
|
|
173
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
174
|
+
__param(0, (0, common_1.Param)('uuid')),
|
|
175
|
+
__param(1, (0, common_1.Param)('key')),
|
|
176
|
+
__metadata("design:type", Function),
|
|
177
|
+
__metadata("design:paramtypes", [String, String]),
|
|
178
|
+
__metadata("design:returntype", Promise)
|
|
179
|
+
], GrantAdminController.prototype, "revoke", null);
|
|
180
|
+
__decorate([
|
|
181
|
+
(0, common_1.Get)('access-bundles'),
|
|
182
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
|
|
183
|
+
__metadata("design:type", Function),
|
|
184
|
+
__metadata("design:paramtypes", []),
|
|
185
|
+
__metadata("design:returntype", Promise)
|
|
186
|
+
], GrantAdminController.prototype, "bundles", null);
|
|
187
|
+
__decorate([
|
|
188
|
+
(0, common_1.Post)('roles/:uuid/access-bundles'),
|
|
189
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
190
|
+
__param(0, (0, common_1.Req)()),
|
|
191
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
192
|
+
__param(2, (0, common_1.Body)()),
|
|
193
|
+
__metadata("design:type", Function),
|
|
194
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
195
|
+
__metadata("design:returntype", Promise)
|
|
196
|
+
], GrantAdminController.prototype, "grantBundle", null);
|
|
197
|
+
__decorate([
|
|
198
|
+
(0, common_1.Delete)('roles/:uuid/access-bundles/:key'),
|
|
199
|
+
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
200
|
+
__param(0, (0, common_1.Param)('uuid')),
|
|
201
|
+
__param(1, (0, common_1.Param)('key')),
|
|
202
|
+
__metadata("design:type", Function),
|
|
203
|
+
__metadata("design:paramtypes", [String, String]),
|
|
204
|
+
__metadata("design:returntype", Promise)
|
|
205
|
+
], GrantAdminController.prototype, "revokeBundle", null);
|
|
206
|
+
exports.GrantAdminController = GrantAdminController = __decorate([
|
|
207
|
+
(0, common_1.Controller)('identity'),
|
|
208
|
+
__metadata("design:paramtypes", [grant_admin_service_js_1.GrantAdminService])
|
|
209
|
+
], GrantAdminController);
|