@astram/identity 0.1.2 → 0.2.1
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 +5 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Pool } from 'mariadb';
|
|
2
|
+
import { PermissionVersionBumper } from './permission-version.bumper.js';
|
|
3
|
+
import { AccessBundleStore } from './access-bundle-store.js';
|
|
4
|
+
/** A granted key was not in the catalog and is not a recognized wildcard. The
|
|
5
|
+
* controller maps this to HTTP 400 (distinct from a store outage → 503). */
|
|
6
|
+
export declare class BadKeyError extends Error {
|
|
7
|
+
constructor(key: string);
|
|
8
|
+
}
|
|
9
|
+
type CatalogRow = {
|
|
10
|
+
key: string;
|
|
11
|
+
label: string;
|
|
12
|
+
module: string;
|
|
13
|
+
typeKey: string;
|
|
14
|
+
action: string;
|
|
15
|
+
};
|
|
16
|
+
/** A bundle as returned by the list API — identified by `key`, NEVER by `ab_uuid`. */
|
|
17
|
+
export interface BundleView {
|
|
18
|
+
key: string;
|
|
19
|
+
module: string;
|
|
20
|
+
label: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
permissionKeys: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A granted role's access summary for the Access listing. `roleUuid` is a key/arg ONLY —
|
|
26
|
+
* the UI shows `roleName`. `grantedByName` is resolved server-side (NEVER a raw uuid).
|
|
27
|
+
*/
|
|
28
|
+
export interface RoleGrantSummary {
|
|
29
|
+
roleUuid: string;
|
|
30
|
+
roleName: string;
|
|
31
|
+
objectCount: number;
|
|
32
|
+
permCount: number;
|
|
33
|
+
chips: string[];
|
|
34
|
+
preset: string | null;
|
|
35
|
+
grantedAt: string | null;
|
|
36
|
+
grantedByName: string | null;
|
|
37
|
+
}
|
|
38
|
+
export declare class GrantAdminService {
|
|
39
|
+
private readonly pool;
|
|
40
|
+
private readonly bumper;
|
|
41
|
+
private readonly bundles;
|
|
42
|
+
constructor(pool: Pool, bumper: PermissionVersionBumper, bundleStore?: AccessBundleStore);
|
|
43
|
+
/** All grantable exact keys (from the catalog) + the computed wildcard set. */
|
|
44
|
+
listCatalog(): Promise<{
|
|
45
|
+
catalog: CatalogRow[];
|
|
46
|
+
wildcards: string[];
|
|
47
|
+
}>;
|
|
48
|
+
listRoleGrants(roleUuid: string): Promise<{
|
|
49
|
+
grants: string[];
|
|
50
|
+
}>;
|
|
51
|
+
/** Validate the key against the catalog + wildcard set. Throws BadKeyError if unknown. */
|
|
52
|
+
private assertValidKey;
|
|
53
|
+
/**
|
|
54
|
+
* Grant a key to a role (idempotent) + bump the version, atomically. `actorUuid`, when
|
|
55
|
+
* given, stamps `rp_granted_at`/`rp_granted_by` on a NEWLY-inserted row (a duplicate is a
|
|
56
|
+
* no-op and its existing stamp is left as-is).
|
|
57
|
+
*/
|
|
58
|
+
grant(roleUuid: string, key: string, actorUuid?: string): Promise<void>;
|
|
59
|
+
/** Revoke a key from a role (no-op if absent) + bump the version, atomically. */
|
|
60
|
+
revoke(roleUuid: string, key: string): Promise<void>;
|
|
61
|
+
/** All active bundles + their resolved atomic keys, for the Access UI. */
|
|
62
|
+
listBundles(): Promise<{
|
|
63
|
+
bundles: BundleView[];
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Grant a bundle to a role: resolve the bundle → its atomic keys, then INSERT each
|
|
67
|
+
* into `role_permission` idempotently in ONE transaction with ONE version bump.
|
|
68
|
+
* An unknown / orphaned bundle → `BadKeyError` (mapped to 400 by the controller).
|
|
69
|
+
*/
|
|
70
|
+
grantBundle(roleUuid: string, bundleKey: string, actorUuid?: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Revoke a bundle from a role: resolve → DELETE each of the bundle's atomic keys, in
|
|
73
|
+
* ONE transaction with ONE version bump. Caveat (design §5.1): this removes exactly the
|
|
74
|
+
* bundle's keys even if a manual grant or another bundle shares one — the model is
|
|
75
|
+
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`.
|
|
76
|
+
*/
|
|
77
|
+
revokeBundle(roleUuid: string, bundleKey: string): Promise<void>;
|
|
78
|
+
/** Resolve a bundle key → its atomic keys; throw BadKeyError when unknown/orphaned. */
|
|
79
|
+
private resolveBundleKeysOrThrow;
|
|
80
|
+
/**
|
|
81
|
+
* Access-listing summary — SERVER-PAGED. One row per role that holds ≥1 grant, with its
|
|
82
|
+
* access summary (chips/counts), the covering preset (if any), and last-modified date +
|
|
83
|
+
* the GRANTOR'S NAME (resolved from `rp_granted_by` → `users.u_username`, never a raw
|
|
84
|
+
* uuid). Wildcard grants are expanded against the live catalog so counts/chips reflect
|
|
85
|
+
* the REAL effective set.
|
|
86
|
+
*
|
|
87
|
+
* Paging model: page over the DISTINCT granted roles (name search + sort + LIMIT/OFFSET
|
|
88
|
+
* + a COUNT for total), then compute the summary only for the page's roles. `sortBy` ∈
|
|
89
|
+
* {name, grantedAt, permCount}; name/grantedAt sort in SQL, permCount sorts the computed
|
|
90
|
+
* page (a per-role aggregate not cheaply sortable in SQL). Defaults: page 1, limit 25,
|
|
91
|
+
* sort name asc.
|
|
92
|
+
*/
|
|
93
|
+
listGrantsSummary(params?: {
|
|
94
|
+
page?: number;
|
|
95
|
+
limit?: number;
|
|
96
|
+
search?: string;
|
|
97
|
+
sortBy?: string;
|
|
98
|
+
sortOrder?: 'asc' | 'desc';
|
|
99
|
+
}): Promise<{
|
|
100
|
+
rows: RoleGrantSummary[];
|
|
101
|
+
total: number;
|
|
102
|
+
page: number;
|
|
103
|
+
limit: number;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
export {};
|
|
107
|
+
//# sourceMappingURL=grant-admin.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GrantAdminService = exports.BadKeyError = void 0;
|
|
4
|
+
const core_1 = require("@astram/core");
|
|
5
|
+
const access_bundle_store_js_1 = require("./access-bundle-store.js");
|
|
6
|
+
const db_permission_facts_adapter_js_1 = require("./db-permission-facts.adapter.js");
|
|
7
|
+
const AUTHZ = 'shield_authz';
|
|
8
|
+
/** A granted key was not in the catalog and is not a recognized wildcard. The
|
|
9
|
+
* controller maps this to HTTP 400 (distinct from a store outage → 503). */
|
|
10
|
+
class BadKeyError extends Error {
|
|
11
|
+
constructor(key) {
|
|
12
|
+
super(`unknown permission key: ${key}`);
|
|
13
|
+
this.name = 'BadKeyError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.BadKeyError = BadKeyError;
|
|
17
|
+
class GrantAdminService {
|
|
18
|
+
pool;
|
|
19
|
+
bumper;
|
|
20
|
+
bundles;
|
|
21
|
+
constructor(pool, bumper, bundleStore) {
|
|
22
|
+
this.pool = pool;
|
|
23
|
+
this.bumper = bumper;
|
|
24
|
+
this.bundles = bundleStore ?? new access_bundle_store_js_1.AccessBundleStore(pool);
|
|
25
|
+
}
|
|
26
|
+
/** All grantable exact keys (from the catalog) + the computed wildcard set. */
|
|
27
|
+
async listCatalog() {
|
|
28
|
+
let rows;
|
|
29
|
+
try {
|
|
30
|
+
rows = (await this.pool.query(`SELECT perm_key, perm_label, perm_module, perm_type_key, perm_action
|
|
31
|
+
FROM \`${AUTHZ}\`.\`permission\`
|
|
32
|
+
WHERE perm_status = 'active'
|
|
33
|
+
ORDER BY perm_type_key, perm_action`));
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
throw new core_1.AuthorizationUnavailableError('permission catalog unavailable', cause);
|
|
37
|
+
}
|
|
38
|
+
const catalog = rows.map((r) => ({
|
|
39
|
+
key: r.perm_key, label: r.perm_label, module: r.perm_module,
|
|
40
|
+
typeKey: r.perm_type_key, action: r.perm_action,
|
|
41
|
+
}));
|
|
42
|
+
// Wildcards: module-wide + one per distinct type key.
|
|
43
|
+
const modules = [...new Set(catalog.map((c) => c.module))];
|
|
44
|
+
const typeKeys = [...new Set(catalog.map((c) => c.typeKey))];
|
|
45
|
+
const wildcards = [
|
|
46
|
+
...modules.map((m) => `${m}:*`),
|
|
47
|
+
...typeKeys.map((t) => `${catalog.find((c) => c.typeKey === t).module}:${t}:*`),
|
|
48
|
+
];
|
|
49
|
+
return { catalog, wildcards };
|
|
50
|
+
}
|
|
51
|
+
async listRoleGrants(roleUuid) {
|
|
52
|
+
try {
|
|
53
|
+
const rows = (await this.pool.query(`SELECT rp_perm_key FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? ORDER BY rp_perm_key`, [roleUuid]));
|
|
54
|
+
return { grants: rows.map((r) => r.rp_perm_key) };
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
throw new core_1.AuthorizationUnavailableError('role grants unavailable', cause);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Validate the key against the catalog + wildcard set. Throws BadKeyError if unknown. */
|
|
61
|
+
async assertValidKey(key) {
|
|
62
|
+
const { catalog, wildcards } = await this.listCatalog();
|
|
63
|
+
const known = new Set([...catalog.map((c) => c.key), ...wildcards]);
|
|
64
|
+
if (!known.has(key))
|
|
65
|
+
throw new BadKeyError(key);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Grant a key to a role (idempotent) + bump the version, atomically. `actorUuid`, when
|
|
69
|
+
* given, stamps `rp_granted_at`/`rp_granted_by` on a NEWLY-inserted row (a duplicate is a
|
|
70
|
+
* no-op and its existing stamp is left as-is).
|
|
71
|
+
*/
|
|
72
|
+
async grant(roleUuid, key, actorUuid) {
|
|
73
|
+
await this.assertValidKey(key); // throws BadKeyError (400) before touching the store
|
|
74
|
+
const conn = await this.pool.getConnection();
|
|
75
|
+
try {
|
|
76
|
+
await conn.beginTransaction();
|
|
77
|
+
// Idempotent insert — UNIQUE (rp_role_uuid, rp_perm_key); a duplicate is a no-op.
|
|
78
|
+
await conn.query(`INSERT INTO \`${AUTHZ}\`.\`role_permission\`
|
|
79
|
+
(rp_uuid, rp_role_uuid, rp_perm_key, rp_granted_at, rp_granted_by)
|
|
80
|
+
SELECT UUID(), ?, ?, UTC_TIMESTAMP(), ?
|
|
81
|
+
WHERE NOT EXISTS (
|
|
82
|
+
SELECT 1 FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?
|
|
83
|
+
)`, [roleUuid, key, actorUuid ?? null, roleUuid, key]);
|
|
84
|
+
await this.bumper.bumpForRole(roleUuid, conn);
|
|
85
|
+
await conn.commit();
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
await conn.rollback();
|
|
89
|
+
if (cause instanceof BadKeyError)
|
|
90
|
+
throw cause;
|
|
91
|
+
throw new core_1.AuthorizationUnavailableError('grant failed', cause);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await conn.release();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Revoke a key from a role (no-op if absent) + bump the version, atomically. */
|
|
98
|
+
async revoke(roleUuid, key) {
|
|
99
|
+
const conn = await this.pool.getConnection();
|
|
100
|
+
try {
|
|
101
|
+
await conn.beginTransaction();
|
|
102
|
+
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, key]);
|
|
103
|
+
await this.bumper.bumpForRole(roleUuid, conn);
|
|
104
|
+
await conn.commit();
|
|
105
|
+
}
|
|
106
|
+
catch (cause) {
|
|
107
|
+
await conn.rollback();
|
|
108
|
+
throw new core_1.AuthorizationUnavailableError('revoke failed', cause);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await conn.release();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ── Access bundles ────────────────────────────────────────────────────────
|
|
115
|
+
/** All active bundles + their resolved atomic keys, for the Access UI. */
|
|
116
|
+
async listBundles() {
|
|
117
|
+
try {
|
|
118
|
+
const bundles = await this.bundles.listActiveBundlesWithKeys();
|
|
119
|
+
return { bundles };
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
throw new core_1.AuthorizationUnavailableError('access bundles unavailable', cause);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Grant a bundle to a role: resolve the bundle → its atomic keys, then INSERT each
|
|
127
|
+
* into `role_permission` idempotently in ONE transaction with ONE version bump.
|
|
128
|
+
* An unknown / orphaned bundle → `BadKeyError` (mapped to 400 by the controller).
|
|
129
|
+
*/
|
|
130
|
+
async grantBundle(roleUuid, bundleKey, actorUuid) {
|
|
131
|
+
const keys = await this.resolveBundleKeysOrThrow(bundleKey);
|
|
132
|
+
const conn = await this.pool.getConnection();
|
|
133
|
+
try {
|
|
134
|
+
await conn.beginTransaction();
|
|
135
|
+
for (const key of keys) {
|
|
136
|
+
// Idempotent — UNIQUE (rp_role_uuid, rp_perm_key); a duplicate is a no-op.
|
|
137
|
+
await conn.query(`INSERT INTO \`${AUTHZ}\`.\`role_permission\`
|
|
138
|
+
(rp_uuid, rp_role_uuid, rp_perm_key, rp_granted_at, rp_granted_by)
|
|
139
|
+
SELECT UUID(), ?, ?, UTC_TIMESTAMP(), ?
|
|
140
|
+
WHERE NOT EXISTS (
|
|
141
|
+
SELECT 1 FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?
|
|
142
|
+
)`, [roleUuid, key, actorUuid ?? null, roleUuid, key]);
|
|
143
|
+
}
|
|
144
|
+
await this.bumper.bumpForRole(roleUuid, conn);
|
|
145
|
+
await conn.commit();
|
|
146
|
+
}
|
|
147
|
+
catch (cause) {
|
|
148
|
+
await conn.rollback();
|
|
149
|
+
throw new core_1.AuthorizationUnavailableError('grant bundle failed', cause);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await conn.release();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Revoke a bundle from a role: resolve → DELETE each of the bundle's atomic keys, in
|
|
157
|
+
* ONE transaction with ONE version bump. Caveat (design §5.1): this removes exactly the
|
|
158
|
+
* bundle's keys even if a manual grant or another bundle shares one — the model is
|
|
159
|
+
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`.
|
|
160
|
+
*/
|
|
161
|
+
async revokeBundle(roleUuid, bundleKey) {
|
|
162
|
+
const keys = await this.resolveBundleKeysOrThrow(bundleKey);
|
|
163
|
+
const conn = await this.pool.getConnection();
|
|
164
|
+
try {
|
|
165
|
+
await conn.beginTransaction();
|
|
166
|
+
for (const key of keys) {
|
|
167
|
+
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, key]);
|
|
168
|
+
}
|
|
169
|
+
await this.bumper.bumpForRole(roleUuid, conn);
|
|
170
|
+
await conn.commit();
|
|
171
|
+
}
|
|
172
|
+
catch (cause) {
|
|
173
|
+
await conn.rollback();
|
|
174
|
+
throw new core_1.AuthorizationUnavailableError('revoke bundle failed', cause);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await conn.release();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Resolve a bundle key → its atomic keys; throw BadKeyError when unknown/orphaned. */
|
|
181
|
+
async resolveBundleKeysOrThrow(bundleKey) {
|
|
182
|
+
let keys;
|
|
183
|
+
try {
|
|
184
|
+
keys = await this.bundles.resolveActiveBundleKeys(bundleKey);
|
|
185
|
+
}
|
|
186
|
+
catch (cause) {
|
|
187
|
+
throw new core_1.AuthorizationUnavailableError('access bundle lookup failed', cause);
|
|
188
|
+
}
|
|
189
|
+
if (keys === null)
|
|
190
|
+
throw new BadKeyError(bundleKey);
|
|
191
|
+
return keys;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Access-listing summary — SERVER-PAGED. One row per role that holds ≥1 grant, with its
|
|
195
|
+
* access summary (chips/counts), the covering preset (if any), and last-modified date +
|
|
196
|
+
* the GRANTOR'S NAME (resolved from `rp_granted_by` → `users.u_username`, never a raw
|
|
197
|
+
* uuid). Wildcard grants are expanded against the live catalog so counts/chips reflect
|
|
198
|
+
* the REAL effective set.
|
|
199
|
+
*
|
|
200
|
+
* Paging model: page over the DISTINCT granted roles (name search + sort + LIMIT/OFFSET
|
|
201
|
+
* + a COUNT for total), then compute the summary only for the page's roles. `sortBy` ∈
|
|
202
|
+
* {name, grantedAt, permCount}; name/grantedAt sort in SQL, permCount sorts the computed
|
|
203
|
+
* page (a per-role aggregate not cheaply sortable in SQL). Defaults: page 1, limit 25,
|
|
204
|
+
* sort name asc.
|
|
205
|
+
*/
|
|
206
|
+
async listGrantsSummary(params) {
|
|
207
|
+
const page = Math.max(1, Math.trunc(params?.page ?? 1));
|
|
208
|
+
const limit = Math.min(200, Math.max(1, Math.trunc(params?.limit ?? 25)));
|
|
209
|
+
const offset = (page - 1) * limit;
|
|
210
|
+
const search = (params?.search ?? '').trim();
|
|
211
|
+
const dir = params?.sortOrder === 'desc' ? 'DESC' : 'ASC';
|
|
212
|
+
const sortBy = params?.sortBy ?? 'name';
|
|
213
|
+
try {
|
|
214
|
+
const { catalog } = await this.listCatalog();
|
|
215
|
+
const allKeys = catalog.map((c) => c.key);
|
|
216
|
+
const { bundles } = await this.listBundles();
|
|
217
|
+
// Distinct granted roles (a role appears iff it has ≥1 role_permission row), filtered
|
|
218
|
+
// by name search. Bind the LIKE term as a param (wildcards escaped).
|
|
219
|
+
const likeClause = search ? `AND r.r_name LIKE ?` : '';
|
|
220
|
+
const likeArgs = search ? [`%${search.replace(/[%_\\]/g, '\\$&')}%`] : [];
|
|
221
|
+
const [{ total = 0 } = {}] = (await this.pool.query(`SELECT COUNT(DISTINCT rp.rp_role_uuid) AS total
|
|
222
|
+
FROM \`${AUTHZ}\`.\`role_permission\` rp
|
|
223
|
+
JOIN \`shield_app\`.\`roles\` r ON r.r_uuid = rp.rp_role_uuid
|
|
224
|
+
WHERE 1 = 1 ${likeClause}`, likeArgs));
|
|
225
|
+
// The page of role uuids + name + latest-grant metadata. name/grantedAt sort in SQL;
|
|
226
|
+
// permCount falls back to a stable name sort here (re-sorted after aggregation).
|
|
227
|
+
const orderCol = sortBy === 'grantedAt' ? 'latest_at' : 'r.r_name';
|
|
228
|
+
const pageRoles = (await this.pool.query(`SELECT rp.rp_role_uuid AS role_uuid,
|
|
229
|
+
r.r_name AS role_name,
|
|
230
|
+
MAX(rp.rp_granted_at) AS latest_at
|
|
231
|
+
FROM \`${AUTHZ}\`.\`role_permission\` rp
|
|
232
|
+
JOIN \`shield_app\`.\`roles\` r ON r.r_uuid = rp.rp_role_uuid
|
|
233
|
+
WHERE 1 = 1 ${likeClause}
|
|
234
|
+
GROUP BY rp.rp_role_uuid, r.r_name
|
|
235
|
+
ORDER BY ${orderCol} ${dir}, r.r_name ASC
|
|
236
|
+
LIMIT ? OFFSET ?`, [...likeArgs, limit, offset]));
|
|
237
|
+
if (pageRoles.length === 0) {
|
|
238
|
+
return { rows: [], total: Number(total), page, limit };
|
|
239
|
+
}
|
|
240
|
+
// Fetch grant keys + grantor names for JUST the page's roles.
|
|
241
|
+
const roleUuids = pageRoles.map((r) => r.role_uuid);
|
|
242
|
+
const ph = roleUuids.map(() => '?').join(', ');
|
|
243
|
+
const grantRows = (await this.pool.query(`SELECT rp.rp_role_uuid AS role_uuid,
|
|
244
|
+
rp.rp_perm_key AS perm_key,
|
|
245
|
+
rp.rp_granted_at AS granted_at,
|
|
246
|
+
u.u_username AS granted_by_name
|
|
247
|
+
FROM \`${AUTHZ}\`.\`role_permission\` rp
|
|
248
|
+
LEFT JOIN \`shield_app\`.\`users\` u ON u.u_uuid = rp.rp_granted_by
|
|
249
|
+
WHERE rp.rp_role_uuid IN (${ph})`, roleUuids));
|
|
250
|
+
const byRole = new Map();
|
|
251
|
+
for (const row of grantRows) {
|
|
252
|
+
let e = byRole.get(row.role_uuid);
|
|
253
|
+
if (!e) {
|
|
254
|
+
e = { rawKeys: [], latestAt: -1, latestBy: null };
|
|
255
|
+
byRole.set(row.role_uuid, e);
|
|
256
|
+
}
|
|
257
|
+
e.rawKeys.push(row.perm_key);
|
|
258
|
+
const ts = row.granted_at ? new Date(row.granted_at).getTime() : -1;
|
|
259
|
+
if (ts > e.latestAt) {
|
|
260
|
+
e.latestAt = ts;
|
|
261
|
+
e.latestBy = row.granted_by_name ?? null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const summaries = pageRoles.map((pr) => {
|
|
265
|
+
const e = byRole.get(pr.role_uuid) ?? { rawKeys: [], latestAt: -1, latestBy: null };
|
|
266
|
+
const effective = new Set((0, db_permission_facts_adapter_js_1.expandGrantKeys)(e.rawKeys, allKeys));
|
|
267
|
+
const typeKeys = new Set();
|
|
268
|
+
for (const key of effective) {
|
|
269
|
+
const parts = key.split(':');
|
|
270
|
+
const typeKey = parts.length === 3 ? parts[1] : undefined;
|
|
271
|
+
if (typeKey)
|
|
272
|
+
typeKeys.add(typeKey);
|
|
273
|
+
}
|
|
274
|
+
const preset = bundles.find((b) => b.permissionKeys.length > 0 && b.permissionKeys.every((k) => effective.has(k)))?.label ?? null;
|
|
275
|
+
return {
|
|
276
|
+
roleUuid: pr.role_uuid,
|
|
277
|
+
roleName: pr.role_name,
|
|
278
|
+
objectCount: typeKeys.size,
|
|
279
|
+
permCount: effective.size,
|
|
280
|
+
chips: [...typeKeys].sort(),
|
|
281
|
+
preset,
|
|
282
|
+
grantedAt: e.latestAt >= 0 ? new Date(e.latestAt).toISOString() : null,
|
|
283
|
+
grantedByName: e.latestBy,
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
// permCount isn't SQL-sortable (per-role aggregate) — sort the page here when asked.
|
|
287
|
+
if (sortBy === 'permCount') {
|
|
288
|
+
summaries.sort((a, b) => dir === 'DESC' ? b.permCount - a.permCount : a.permCount - b.permCount);
|
|
289
|
+
}
|
|
290
|
+
return { rows: summaries, total: Number(total), page, limit };
|
|
291
|
+
}
|
|
292
|
+
catch (cause) {
|
|
293
|
+
throw new core_1.AuthorizationUnavailableError('grants summary unavailable', cause);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
exports.GrantAdminService = GrantAdminService;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Pool } from 'mariadb';
|
|
2
|
+
import type { CatalogStore, CatalogPermissionRow } from '@astram/guard';
|
|
3
|
+
/**
|
|
4
|
+
* CatalogStore impl over shield_authz.permission (raw parameterized SQL, mirroring
|
|
5
|
+
* permission-facts.adapter). The reconciler (framework) calls this; guard does no DB
|
|
6
|
+
* itself (ADR-0003).
|
|
7
|
+
*/
|
|
8
|
+
export declare class MariaDbCatalogStore implements CatalogStore {
|
|
9
|
+
private readonly pool;
|
|
10
|
+
constructor(pool: Pool);
|
|
11
|
+
listPermissions(): Promise<CatalogPermissionRow[]>;
|
|
12
|
+
upsertPermission(row: Omit<CatalogPermissionRow, 'status'>): Promise<boolean>;
|
|
13
|
+
markOrphaned(keys: readonly string[]): Promise<void>;
|
|
14
|
+
markActive(keys: readonly string[]): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=mariadb-catalog-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mariadb-catalog-store.d.ts","sourceRoot":"","sources":["../../../src/features/authz/mariadb-catalog-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAIxE;;;;GAIG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,eAAe,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAelD,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IAW7E,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IASpD,UAAU,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAQzD"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MariaDbCatalogStore = void 0;
|
|
4
|
+
const AUTHZ = 'shield_authz';
|
|
5
|
+
/**
|
|
6
|
+
* CatalogStore impl over shield_authz.permission (raw parameterized SQL, mirroring
|
|
7
|
+
* permission-facts.adapter). The reconciler (framework) calls this; guard does no DB
|
|
8
|
+
* itself (ADR-0003).
|
|
9
|
+
*/
|
|
10
|
+
class MariaDbCatalogStore {
|
|
11
|
+
pool;
|
|
12
|
+
constructor(pool) {
|
|
13
|
+
this.pool = pool;
|
|
14
|
+
}
|
|
15
|
+
async listPermissions() {
|
|
16
|
+
const rows = (await this.pool.query(`SELECT perm_key, perm_module, perm_type_key, perm_action, perm_label, perm_tier, perm_status
|
|
17
|
+
FROM \`${AUTHZ}\`.\`permission\``));
|
|
18
|
+
return rows.map((r) => ({
|
|
19
|
+
permissionKey: r.perm_key, moduleKey: r.perm_module, typeKey: r.perm_type_key,
|
|
20
|
+
action: r.perm_action, label: r.perm_label, tier: r.perm_tier,
|
|
21
|
+
status: r.perm_status === 'orphaned' ? 'orphaned' : 'active',
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
async upsertPermission(row) {
|
|
25
|
+
const res = (await this.pool.query(`INSERT INTO \`${AUTHZ}\`.\`permission\`
|
|
26
|
+
(perm_uuid, perm_key, perm_module, perm_type_key, perm_action, perm_label, perm_tier, perm_status)
|
|
27
|
+
SELECT UUID(), ?, ?, ?, ?, ?, ?, 'active'
|
|
28
|
+
WHERE NOT EXISTS (SELECT 1 FROM \`${AUTHZ}\`.\`permission\` WHERE perm_key = ?)`, [row.permissionKey, row.moduleKey, row.typeKey, row.action, row.label, row.tier, row.permissionKey]));
|
|
29
|
+
return Number(res?.affectedRows ?? 0) > 0;
|
|
30
|
+
}
|
|
31
|
+
async markOrphaned(keys) {
|
|
32
|
+
if (keys.length === 0)
|
|
33
|
+
return;
|
|
34
|
+
const ph = keys.map(() => '?').join(', ');
|
|
35
|
+
await this.pool.query(`UPDATE \`${AUTHZ}\`.\`permission\` SET perm_status = 'orphaned' WHERE perm_key IN (${ph})`, [...keys]);
|
|
36
|
+
}
|
|
37
|
+
async markActive(keys) {
|
|
38
|
+
if (keys.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
const ph = keys.map(() => '?').join(', ');
|
|
41
|
+
await this.pool.query(`UPDATE \`${AUTHZ}\`.\`permission\` SET perm_status = 'active' WHERE perm_key IN (${ph})`, [...keys]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.MariaDbCatalogStore = MariaDbCatalogStore;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The minimal query surface this writer needs — a `mariadb` `Pool` (or `PoolConnection`)
|
|
3
|
+
* satisfies it structurally, but the type itself stays framework-agnostic so this signature
|
|
4
|
+
* doesn't force `mariadb` on callers (mirrors `RuleQueryable` in `@astram/guard`).
|
|
5
|
+
*/
|
|
6
|
+
export interface Queryable {
|
|
7
|
+
query(sql: string, params?: readonly unknown[]): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* PermissionVersionBumper — the WRITER side of ADR-0011: increments a tenant's
|
|
11
|
+
* `shield_authz.authz_permission_version` so `loadCoarseGrants` marks cached facts stale →
|
|
12
|
+
* gate 5 reloads → a role gain/loss (from a posting change) lands WITHOUT re-login. Runs the
|
|
13
|
+
* INSERT-or-increment over a supplied connection (the sealed authz pool, or a transaction
|
|
14
|
+
* connection so the bump joins the caller's write). Fail-safe: the caller wraps a bump failure
|
|
15
|
+
* so it never fails the membership/posting write. Never logs the tenant or the query.
|
|
16
|
+
*
|
|
17
|
+
* NOTE — the two bump methods do NOT share an error contract:
|
|
18
|
+
* - `bumpForTenant` propagates the raw underlying error unwrapped; it is up to ITS caller
|
|
19
|
+
* (the Phase 3 membership/posting write) to swallow it fail-safe, per the paragraph above.
|
|
20
|
+
* - `bumpForRole` wraps a failure in `AuthorizationUnavailableError` and THROWS (see its own
|
|
21
|
+
* doc comment below) — do not assume it is also fail-safe.
|
|
22
|
+
*/
|
|
23
|
+
export declare class PermissionVersionBumper {
|
|
24
|
+
/**
|
|
25
|
+
* Increment the permission version for a SINGLE tenant — for events that change a
|
|
26
|
+
* principal's EFFECTIVE grants without touching `role_permission` directly (a
|
|
27
|
+
* MEMBERSHIP/POSTING change: a user gaining or losing a role in a company).
|
|
28
|
+
*/
|
|
29
|
+
bumpForTenant(tenantUuid: string, conn: Queryable): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Increment the permission version for EVERY tenant where the role is ACTIVELY posted —
|
|
32
|
+
* for a role↔permission GRANT/REVOKE (which changes the effective grants of every principal
|
|
33
|
+
* holding that role in a company). Finds the tenants via the active membership+posting join,
|
|
34
|
+
* bumps each. Returns the bumped tenant uuids. A role with no active postings bumps nothing.
|
|
35
|
+
*
|
|
36
|
+
* NOTE — unlike `bumpForTenant` (which propagates its error raw and relies on the caller to
|
|
37
|
+
* swallow it fail-safe), THIS method wraps any failure in `AuthorizationUnavailableError` and
|
|
38
|
+
* THROWS: a role grant/revoke MUST fail loudly if the version bump can't be applied — a
|
|
39
|
+
* silently-stale bump would leave a removed grant still honored. The grant-admin caller
|
|
40
|
+
* decides how to handle it (ideally in the same transaction as the `role_permission` write,
|
|
41
|
+
* so both roll back together).
|
|
42
|
+
*/
|
|
43
|
+
bumpForRole(roleUuid: string, conn: Queryable): Promise<string[]>;
|
|
44
|
+
/** The raw per-tenant increment (INSERT-or-increment). */
|
|
45
|
+
private bumpTenant;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=permission-version.bumper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-version.bumper.d.ts","sourceRoot":"","sources":["../../../src/features/authz/permission-version.bumper.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,uBAAuB;IAClC;;;;OAIG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvE;;;;;;;;;;;;OAYG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBvE,0DAA0D;YAC5C,UAAU;CAQzB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PermissionVersionBumper = void 0;
|
|
4
|
+
const core_1 = require("@astram/core");
|
|
5
|
+
/** Schema the `authz_permission_version` table lives in (ADR-0011). */
|
|
6
|
+
const AUTHZ = 'shield_authz';
|
|
7
|
+
/** Schema the membership/posting tables live in — the join `bumpForRole` walks. */
|
|
8
|
+
const APP = 'shield_app';
|
|
9
|
+
/**
|
|
10
|
+
* PermissionVersionBumper — the WRITER side of ADR-0011: increments a tenant's
|
|
11
|
+
* `shield_authz.authz_permission_version` so `loadCoarseGrants` marks cached facts stale →
|
|
12
|
+
* gate 5 reloads → a role gain/loss (from a posting change) lands WITHOUT re-login. Runs the
|
|
13
|
+
* INSERT-or-increment over a supplied connection (the sealed authz pool, or a transaction
|
|
14
|
+
* connection so the bump joins the caller's write). Fail-safe: the caller wraps a bump failure
|
|
15
|
+
* so it never fails the membership/posting write. Never logs the tenant or the query.
|
|
16
|
+
*
|
|
17
|
+
* NOTE — the two bump methods do NOT share an error contract:
|
|
18
|
+
* - `bumpForTenant` propagates the raw underlying error unwrapped; it is up to ITS caller
|
|
19
|
+
* (the Phase 3 membership/posting write) to swallow it fail-safe, per the paragraph above.
|
|
20
|
+
* - `bumpForRole` wraps a failure in `AuthorizationUnavailableError` and THROWS (see its own
|
|
21
|
+
* doc comment below) — do not assume it is also fail-safe.
|
|
22
|
+
*/
|
|
23
|
+
class PermissionVersionBumper {
|
|
24
|
+
/**
|
|
25
|
+
* Increment the permission version for a SINGLE tenant — for events that change a
|
|
26
|
+
* principal's EFFECTIVE grants without touching `role_permission` directly (a
|
|
27
|
+
* MEMBERSHIP/POSTING change: a user gaining or losing a role in a company).
|
|
28
|
+
*/
|
|
29
|
+
async bumpForTenant(tenantUuid, conn) {
|
|
30
|
+
await this.bumpTenant(tenantUuid, conn);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Increment the permission version for EVERY tenant where the role is ACTIVELY posted —
|
|
34
|
+
* for a role↔permission GRANT/REVOKE (which changes the effective grants of every principal
|
|
35
|
+
* holding that role in a company). Finds the tenants via the active membership+posting join,
|
|
36
|
+
* bumps each. Returns the bumped tenant uuids. A role with no active postings bumps nothing.
|
|
37
|
+
*
|
|
38
|
+
* NOTE — unlike `bumpForTenant` (which propagates its error raw and relies on the caller to
|
|
39
|
+
* swallow it fail-safe), THIS method wraps any failure in `AuthorizationUnavailableError` and
|
|
40
|
+
* THROWS: a role grant/revoke MUST fail loudly if the version bump can't be applied — a
|
|
41
|
+
* silently-stale bump would leave a removed grant still honored. The grant-admin caller
|
|
42
|
+
* decides how to handle it (ideally in the same transaction as the `role_permission` write,
|
|
43
|
+
* so both roll back together).
|
|
44
|
+
*/
|
|
45
|
+
async bumpForRole(roleUuid, conn) {
|
|
46
|
+
try {
|
|
47
|
+
const rows = (await conn.query(`SELECT DISTINCT cm.cm_company_uuid AS tenant
|
|
48
|
+
FROM \`${APP}\`.\`company_membership\` cm
|
|
49
|
+
JOIN \`${APP}\`.\`membership_posting\` mp ON mp.mp_membership_uuid = cm.cm_uuid
|
|
50
|
+
WHERE mp.mp_role_uuid = ? AND cm.cm_is_active = 1`, [roleUuid]));
|
|
51
|
+
const tenants = rows.map((r) => r.tenant);
|
|
52
|
+
for (const tenant of tenants) {
|
|
53
|
+
await this.bumpTenant(tenant, conn);
|
|
54
|
+
}
|
|
55
|
+
return tenants;
|
|
56
|
+
}
|
|
57
|
+
catch (cause) {
|
|
58
|
+
throw new core_1.AuthorizationUnavailableError('permission version bump failed', cause);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The raw per-tenant increment (INSERT-or-increment). */
|
|
62
|
+
async bumpTenant(tenantUuid, conn) {
|
|
63
|
+
await conn.query(`INSERT INTO \`${AUTHZ}\`.\`authz_permission_version\` (apv_tenant_uuid, apv_version)
|
|
64
|
+
VALUES (?, 1)
|
|
65
|
+
ON DUPLICATE KEY UPDATE apv_version = apv_version + 1`, [tenantUuid]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.PermissionVersionBumper = PermissionVersionBumper;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble the record-tier authorization wiring for `@astram/identity`: the lean grant-read
|
|
3
|
+
* pool factory + the `recordAuthorization` inputs the consumer passes to
|
|
4
|
+
* `ShieldModule.forRoot({ recordAuthorization })`. Once wired, `decide()` becomes REAL (the
|
|
5
|
+
* framework's four record-tier adapters are built internally over the supplied pool) instead
|
|
6
|
+
* of the deny-everything stub.
|
|
7
|
+
*
|
|
8
|
+
* Modeled on astram3's `buildAuthorizationInputs` (`server/src/features/authz/authz.wiring.ts`),
|
|
9
|
+
* adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix
|
|
10
|
+
* (`APP_DB_*`, per the Task-7 rename).
|
|
11
|
+
*/
|
|
12
|
+
import { type Pool } from 'mariadb';
|
|
13
|
+
import { type RecordAuthorizationInputs } from '@astram/guard';
|
|
14
|
+
/**
|
|
15
|
+
* A lean `mariadb` pool for the authz grant-reads (built from `APP_DB_*` env via guard's
|
|
16
|
+
* `loadDbConfig`). Kept small — these are pre-gate coarse-grant reads, not the main CRUD pool.
|
|
17
|
+
* Opens the pool at CALL time (never at import) — the consumer (app lifecycle) owns closing it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createIdentityAuthzPool(): Pool;
|
|
20
|
+
/**
|
|
21
|
+
* Build the `RecordAuthorizationInputs` the consumer passes to
|
|
22
|
+
* `ShieldModule.forRoot({ recordAuthorization })`. The framework owns the four record-tier
|
|
23
|
+
* adapters internally; `@astram/identity` supplies only the app-schema ports:
|
|
24
|
+
* - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
|
|
25
|
+
* `company_membership`/`membership_posting`,
|
|
26
|
+
* - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
|
|
27
|
+
* - `registry` — bridged from `identityObjects`,
|
|
28
|
+
* - `operationManifest` — empty (identity declares no trusted delegated operations).
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildIdentityRecordAuthorization(pool: Pool): RecordAuthorizationInputs;
|
|
31
|
+
//# sourceMappingURL=record-authorization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-authorization.d.ts","sourceRoot":"","sources":["../../../src/features/authz/record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAA6C,KAAK,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAK1G;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED;;;;;;;;;GASG;AACH,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,IAAI,GAAG,yBAAyB,CAStF"}
|