@astram/identity 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/company-membership.object.d.ts.map +1 -1
- package/dist/company-membership.object.js +3 -2
- package/dist/company.object.d.ts.map +1 -1
- package/dist/company.object.js +8 -3
- package/dist/features/access/index.d.ts.map +1 -1
- package/dist/features/access/index.js +1 -5
- package/dist/features/audit/audit-read.service.d.ts.map +1 -1
- package/dist/features/audit/audit-read.service.js +13 -6
- package/dist/features/audit/audit.controller.d.ts.map +1 -1
- package/dist/features/audit/audit.controller.js +2 -0
- package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
- package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/catalog-reconcile.job.js +1 -3
- package/dist/features/authz/db-permission-facts.adapter.d.ts +16 -0
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
- package/dist/features/authz/db-permission-facts.adapter.js +46 -2
- package/dist/features/authz/grant-admin.controller.d.ts +8 -5
- package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.controller.js +20 -18
- package/dist/features/authz/grant-admin.service.d.ts +72 -16
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +232 -86
- package/dist/features/authz/grant-ceiling.d.ts +100 -1
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +264 -2
- package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
- package/dist/features/authz/mariadb-catalog-store.js +15 -3
- package/dist/features/authz/permission-version.bumper.d.ts +52 -1
- package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
- package/dist/features/authz/permission-version.bumper.js +119 -20
- package/dist/features/authz/record-authorization.d.ts +12 -0
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +49 -0
- package/dist/features/authz/role-binding.d.ts +36 -0
- package/dist/features/authz/role-binding.d.ts.map +1 -0
- package/dist/features/authz/role-binding.js +53 -0
- package/dist/features/company/company-hierarchy.service.d.ts +108 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
- package/dist/features/company/company-hierarchy.service.js +277 -0
- package/dist/features/company/company.controller.d.ts +23 -5
- package/dist/features/company/company.controller.d.ts.map +1 -1
- package/dist/features/company/company.controller.js +47 -12
- package/dist/features/company/company.service.d.ts +28 -12
- package/dist/features/company/company.service.d.ts.map +1 -1
- package/dist/features/company/company.service.js +61 -14
- package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
- package/dist/features/cross-tenant/cross-tenant-grant.controller.js +2 -0
- package/dist/features/identity-feature.module.d.ts +2 -0
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +72 -5
- package/dist/features/membership/membership.controller.d.ts +6 -5
- package/dist/features/membership/membership.controller.d.ts.map +1 -1
- package/dist/features/membership/membership.controller.js +34 -22
- package/dist/features/membership/membership.service.d.ts +20 -14
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +62 -29
- package/dist/features/membership/posting-version-bump.d.ts +12 -0
- package/dist/features/membership/posting-version-bump.d.ts.map +1 -1
- package/dist/features/membership/posting.service.d.ts +20 -24
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +101 -64
- package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.controller.js +2 -0
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +4 -1
- package/dist/features/policy/policy.controller.d.ts.map +1 -1
- package/dist/features/policy/policy.controller.js +2 -0
- package/dist/features/role/role.controller.d.ts +3 -2
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +15 -10
- package/dist/features/role/role.service.d.ts +39 -8
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +77 -10
- package/dist/features/scope/scope-closure.service.d.ts +54 -4
- package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
- package/dist/features/scope/scope-closure.service.js +237 -85
- package/dist/features/service-account/service-account.controller.d.ts +3 -2
- package/dist/features/service-account/service-account.controller.d.ts.map +1 -1
- package/dist/features/service-account/service-account.controller.js +29 -15
- package/dist/features/service-account/service-account.service.d.ts +29 -7
- package/dist/features/service-account/service-account.service.d.ts.map +1 -1
- package/dist/features/service-account/service-account.service.js +73 -12
- package/dist/features/service-account/service-account.store.d.ts +15 -0
- package/dist/features/service-account/service-account.store.d.ts.map +1 -1
- package/dist/features/service-account/service-account.store.js +29 -0
- package/dist/features/share/share.controller.d.ts.map +1 -1
- package/dist/features/share/share.controller.js +2 -0
- package/dist/features/share/share.service.d.ts.map +1 -1
- package/dist/features/shared/identity-error.interceptor.d.ts +24 -0
- package/dist/features/shared/identity-error.interceptor.d.ts.map +1 -0
- package/dist/features/shared/identity-error.interceptor.js +82 -0
- package/dist/features/shared/identity-errors.d.ts +23 -0
- package/dist/features/shared/identity-errors.d.ts.map +1 -0
- package/dist/features/shared/identity-errors.js +41 -0
- package/dist/features/user/session-revocation-check.d.ts +11 -0
- package/dist/features/user/session-revocation-check.d.ts.map +1 -0
- package/dist/features/user/session-revocation-check.js +21 -0
- package/dist/features/user/user.controller.d.ts +11 -6
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +35 -17
- package/dist/features/user/user.service.d.ts +55 -14
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +143 -16
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/internal.d.ts +10 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +13 -0
- package/dist/membership-posting.object.d.ts.map +1 -1
- package/dist/membership-posting.object.js +4 -2
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +13 -3
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +3 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +3 -0
- package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
- package/package.json +15 -4
- package/dist/auth-facts-keys.js.map +0 -1
- package/dist/branch.object.js.map +0 -1
- package/dist/company-branch.object.js.map +0 -1
- package/dist/company-department.object.js.map +0 -1
- package/dist/company-membership.object.js.map +0 -1
- package/dist/company.object.js.map +0 -1
- package/dist/cross-tenant-grant.object.js.map +0 -1
- package/dist/department.object.js.map +0 -1
- package/dist/features/access/access-admin.access.js.map +0 -1
- package/dist/features/access/company-admin.access.js.map +0 -1
- package/dist/features/access/index.js.map +0 -1
- package/dist/features/access/user-management.access.js.map +0 -1
- package/dist/features/audit/audit-read.service.js.map +0 -1
- package/dist/features/audit/audit.controller.js.map +0 -1
- package/dist/features/audit/directory-label-resolver.js.map +0 -1
- package/dist/features/audit/mariadb-audit-reader.js.map +0 -1
- package/dist/features/authz/access-bundle-reconcile.job.js.map +0 -1
- package/dist/features/authz/access-bundle-store.js.map +0 -1
- package/dist/features/authz/catalog-bootstrap.provider.js.map +0 -1
- package/dist/features/authz/catalog-reconcile.job.js.map +0 -1
- package/dist/features/authz/db-permission-facts.adapter.js.map +0 -1
- package/dist/features/authz/grant-admin.controller.js.map +0 -1
- package/dist/features/authz/grant-admin.service.js.map +0 -1
- package/dist/features/authz/grant-ceiling.js.map +0 -1
- package/dist/features/authz/mariadb-catalog-store.js.map +0 -1
- package/dist/features/authz/permission-version.bumper.js.map +0 -1
- package/dist/features/authz/record-authorization.js.map +0 -1
- package/dist/features/authz/session-facts.reader.js.map +0 -1
- package/dist/features/company/company.controller.js.map +0 -1
- package/dist/features/company/company.service.js.map +0 -1
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.js.map +0 -1
- package/dist/features/cross-tenant/cross-tenant-grant.controller.js.map +0 -1
- package/dist/features/cross-tenant/grant-authorize.js.map +0 -1
- package/dist/features/identity-feature.module.js.map +0 -1
- package/dist/features/membership/membership-checker.js.map +0 -1
- package/dist/features/membership/membership.controller.js.map +0 -1
- package/dist/features/membership/membership.service.js.map +0 -1
- package/dist/features/membership/posting-version-bump.js.map +0 -1
- package/dist/features/membership/posting.service.js.map +0 -1
- package/dist/features/org-structure/org-catalog.controller.js.map +0 -1
- package/dist/features/org-structure/org-catalog.service.js.map +0 -1
- package/dist/features/policy/policy.controller.js.map +0 -1
- package/dist/features/policy/policy.service.js.map +0 -1
- package/dist/features/role/role.controller.js.map +0 -1
- package/dist/features/role/role.service.js.map +0 -1
- package/dist/features/scope/scope-closure.service.js.map +0 -1
- package/dist/features/scope/scope-subtree.resolver.js.map +0 -1
- package/dist/features/share/share.controller.js.map +0 -1
- package/dist/features/share/share.service.js.map +0 -1
- package/dist/features/shared/http.js.map +0 -1
- package/dist/features/shared/list-query.js.map +0 -1
- package/dist/features/shared/named-option.js.map +0 -1
- package/dist/features/shared/record-resolver.js.map +0 -1
- package/dist/features/user/user.controller.js.map +0 -1
- package/dist/features/user/user.service.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/membership-posting.object.js.map +0 -1
- package/dist/migrations-dir.js.map +0 -1
- package/dist/objects.js.map +0 -1
- package/dist/record-share.object.js.map +0 -1
- package/dist/role.object.js.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/shared-resource.object.js.map +0 -1
- package/dist/user.object.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,qBAca,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,qBAca,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;IAId,QAAQ,EAAG,MAAM,CAAC;IAGlB,WAAW,EAAG,MAAM,CAAC;IAGrB,QAAQ,EAAG,OAAO,CAAC;IAGnB,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
|
|
@@ -21,6 +21,7 @@ const schema_1 = require("./schema");
|
|
|
21
21
|
*/
|
|
22
22
|
let CompanyMembership = class CompanyMembership {
|
|
23
23
|
uuid;
|
|
24
|
+
// immutable (ADR-0034): a membership never changes its user or company after create.
|
|
24
25
|
userUuid;
|
|
25
26
|
companyUuid;
|
|
26
27
|
isActive;
|
|
@@ -33,11 +34,11 @@ __decorate([
|
|
|
33
34
|
__metadata("design:type", String)
|
|
34
35
|
], CompanyMembership.prototype, "uuid", void 0);
|
|
35
36
|
__decorate([
|
|
36
|
-
(0, engine_1.Field)({ column: 'cm_user_uuid', type: 'uuid', required: true }),
|
|
37
|
+
(0, engine_1.Field)({ column: 'cm_user_uuid', type: 'uuid', required: true, immutable: true }),
|
|
37
38
|
__metadata("design:type", String)
|
|
38
39
|
], CompanyMembership.prototype, "userUuid", void 0);
|
|
39
40
|
__decorate([
|
|
40
|
-
(0, engine_1.Field)({ column: 'cm_company_uuid', type: 'uuid', required: true }),
|
|
41
|
+
(0, engine_1.Field)({ column: 'cm_company_uuid', type: 'uuid', required: true, immutable: true }),
|
|
41
42
|
__metadata("design:type", String)
|
|
42
43
|
], CompanyMembership.prototype, "companyUuid", void 0);
|
|
43
44
|
__decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company.object.d.ts","sourceRoot":"","sources":["../src/company.object.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,
|
|
1
|
+
{"version":3,"file":"company.object.d.ts","sourceRoot":"","sources":["../src/company.object.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,qBAsBa,OAAO;IAElB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAKd,UAAU,EAAG,MAAM,CAAC;IAGpB,IAAI,EAAG,MAAM,CAAC;IAGd,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
|
package/dist/company.object.js
CHANGED
|
@@ -16,6 +16,8 @@ const schema_1 = require("./schema");
|
|
|
16
16
|
let Company = class Company {
|
|
17
17
|
uuid;
|
|
18
18
|
name;
|
|
19
|
+
// immutable (ADR-0034): re-parenting is a hierarchy move (cycle check, closure rebuild,
|
|
20
|
+
// session bump), never a side effect of a generic patch.
|
|
19
21
|
parentUuid;
|
|
20
22
|
type;
|
|
21
23
|
status;
|
|
@@ -31,11 +33,11 @@ __decorate([
|
|
|
31
33
|
__metadata("design:type", String)
|
|
32
34
|
], Company.prototype, "name", void 0);
|
|
33
35
|
__decorate([
|
|
34
|
-
(0, engine_1.Field)({ column: 'c_parent_uuid', type: 'uuid' }),
|
|
36
|
+
(0, engine_1.Field)({ column: 'c_parent_uuid', type: 'uuid', immutable: true }),
|
|
35
37
|
__metadata("design:type", String)
|
|
36
38
|
], Company.prototype, "parentUuid", void 0);
|
|
37
39
|
__decorate([
|
|
38
|
-
(0, engine_1.Field)({ column: 'c_type', type: 'string' }),
|
|
40
|
+
(0, engine_1.Field)({ column: 'c_type', type: 'string', immutable: true }),
|
|
39
41
|
__metadata("design:type", String)
|
|
40
42
|
], Company.prototype, "type", void 0);
|
|
41
43
|
__decorate([
|
|
@@ -63,7 +65,10 @@ exports.Company = Company = __decorate([
|
|
|
63
65
|
// the node (`c_uuid`), so a non-root LIST/GET is bounded to the caller's reachable subtree
|
|
64
66
|
// (`c_uuid` IN my subtree). DECOUPLED from writes (no `trusted` change) — this filters
|
|
65
67
|
// reads only. Root spans all.
|
|
66
|
-
|
|
68
|
+
// create:'unscoped' (ADR-0034): a NEW company's c_uuid is not in anyone's subtree until the
|
|
69
|
+
// scope closure lists it, so the post-insert create wall would deny every create. Opted out
|
|
70
|
+
// explicitly; the company service owns where a non-root create may attach (parent stamping).
|
|
71
|
+
readScope: { kind: 'nodeColumn', column: 'c_uuid', create: 'unscoped' },
|
|
67
72
|
}),
|
|
68
73
|
(0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'c_uuid' })
|
|
69
74
|
], Company);
|
|
@@ -1 +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,
|
|
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,iCAAgE,CAAC"}
|
|
@@ -19,8 +19,4 @@ Object.defineProperty(exports, "CompanyAdminAccess", { enumerable: true, get: fu
|
|
|
19
19
|
var access_admin_access_2 = require("./access-admin.access");
|
|
20
20
|
Object.defineProperty(exports, "AccessAdminAccess", { enumerable: true, get: function () { return access_admin_access_2.AccessAdminAccess; } });
|
|
21
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
|
-
];
|
|
22
|
+
exports.identityAccessBundles = [user_management_access_1.UserManagementAccess, company_admin_access_1.CompanyAdminAccess, access_admin_access_1.AccessAdminAccess];
|
|
@@ -1 +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;
|
|
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;QACP,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,GACA,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;IAkE5E;;;OAGG;IACH,OAAO,CAAC,QAAQ;CAUjB"}
|
|
@@ -33,7 +33,10 @@ class AuditReadService {
|
|
|
33
33
|
// and tenantFilter is ignored (cannot widen or cross).
|
|
34
34
|
let tenantId;
|
|
35
35
|
if (principal.isRoot) {
|
|
36
|
-
tenantId =
|
|
36
|
+
tenantId =
|
|
37
|
+
filters.tenantFilter && filters.tenantFilter.length > 0
|
|
38
|
+
? filters.tenantFilter
|
|
39
|
+
: ALL_TENANTS;
|
|
37
40
|
}
|
|
38
41
|
else {
|
|
39
42
|
tenantId = principal.tenantId;
|
|
@@ -47,9 +50,13 @@ class AuditReadService {
|
|
|
47
50
|
// Clamp the client-supplied limit to [1, MAX_LIMIT]; a non-finite value falls back to
|
|
48
51
|
// DEFAULT_LIMIT (only an in-range-but-out-of-bounds number gets floored/capped). Floor the
|
|
49
52
|
// page to >= 1. offset uses the CLAMPED limit, so the window never diverges from read.limit.
|
|
50
|
-
const requestedLimit = Number.isFinite(filters.limit)
|
|
53
|
+
const requestedLimit = Number.isFinite(filters.limit)
|
|
54
|
+
? filters.limit
|
|
55
|
+
: DEFAULT_LIMIT;
|
|
51
56
|
const limit = Math.min(Math.max(1, requestedLimit), MAX_LIMIT);
|
|
52
|
-
const page = Number.isFinite(filters.page) && filters.page >= 1
|
|
57
|
+
const page = Number.isFinite(filters.page) && filters.page >= 1
|
|
58
|
+
? Math.floor(filters.page)
|
|
59
|
+
: 1;
|
|
53
60
|
const offset = (page - 1) * limit;
|
|
54
61
|
// ONE scope query — spread into BOTH the count and the page read so their tenant/type/actor
|
|
55
62
|
// WHERE is identical. A non-root's total therefore counts ONLY its own tenant (invariant #4).
|
|
@@ -69,9 +76,9 @@ class AuditReadService {
|
|
|
69
76
|
const rows = await Promise.all(events.map(async (event) => ({
|
|
70
77
|
occurredAt: event.occurredAtMs,
|
|
71
78
|
actorLabel: await this.labels.resolve('principal', event.actorId),
|
|
72
|
-
action: event.action?.key
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
action: event.action?.key ??
|
|
80
|
+
event.action ??
|
|
81
|
+
'(event)',
|
|
75
82
|
tenantLabel: await this.labels.resolve('tenant', event.tenantId),
|
|
76
83
|
detail: this.stripIds(event.attributes ?? {}),
|
|
77
84
|
})));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.controller.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"audit.controller.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit.controller.ts"],"names":[],"mappings":"AAWA,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,qBAEa,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"}
|
|
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.AuditController = void 0;
|
|
16
16
|
// audit.controller.ts
|
|
17
17
|
const common_1 = require("@nestjs/common");
|
|
18
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
18
19
|
const guard_1 = require("@astram/guard");
|
|
19
20
|
// `AuditReadService` is the DI token for the constructor injection below, so it MUST be a
|
|
20
21
|
// value import — a type-only import is erased at compile time, leaving NestJS with
|
|
@@ -69,5 +70,6 @@ __decorate([
|
|
|
69
70
|
], AuditController.prototype, "list", null);
|
|
70
71
|
exports.AuditController = AuditController = __decorate([
|
|
71
72
|
(0, common_1.Controller)('identity'),
|
|
73
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
72
74
|
__metadata("design:paramtypes", [audit_read_service_js_1.AuditReadService])
|
|
73
75
|
], AuditController);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-audit-reader.d.ts","sourceRoot":"","sources":["../../../src/features/audit/mariadb-audit-reader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAgBD;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,WAAW;IACpD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,EAAE,EAAE,cAAc,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;IAKxD,IAAI,CAAC,KAAK,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;IAgClC;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAY1F;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;
|
|
1
|
+
{"version":3,"file":"mariadb-audit-reader.d.ts","sourceRoot":"","sources":["../../../src/features/audit/mariadb-audit-reader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAgBD;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,WAAW;IACpD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,EAAE,EAAE,cAAc,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;IAKxD,IAAI,CAAC,KAAK,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;IAgClC;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAY1F;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,OAAO;CA+BhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-bundle-reconcile.job.d.ts","sourceRoot":"","sources":["../../../src/features/authz/access-bundle-reconcile.job.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKvD,yCAAyC;AACzC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,wBAAwB;IAKjC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,OAAO,CAAS;gBAGL,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,SAAS,gBAAgB,EAAE;IAKvD,6EAA6E;IACvE,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"access-bundle-reconcile.job.d.ts","sourceRoot":"","sources":["../../../src/features/authz/access-bundle-reconcile.job.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKvD,yCAAyC;AACzC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,wBAAwB;IAKjC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,OAAO,CAAS;gBAGL,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,SAAS,gBAAgB,EAAE;IAKvD,6EAA6E;IACvE,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAqDpE,sEAAsE;YACxD,wBAAwB;CAMvC"}
|
|
@@ -1 +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;
|
|
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;IAQ5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAP3B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAS;gBAGtB,IAAI,EAAE,IAAI,EACO,QAAQ,EAAE,cAAc;IAK3C,6EAA6E;IACvE,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAiB9D,SAAS,IAAI,eAAe,GAAG,IAAI;CAGpC"}
|
|
@@ -29,9 +29,7 @@ class CatalogReconcileJob {
|
|
|
29
29
|
try {
|
|
30
30
|
const report = await this.reconciler.reconcile(this.registry, this.store, nowIso);
|
|
31
31
|
this.latest = report;
|
|
32
|
-
const orphanNote = report.orphaned.length
|
|
33
|
-
? ` — ORPHANED: ${report.orphaned.join(', ')}`
|
|
34
|
-
: '';
|
|
32
|
+
const orphanNote = report.orphaned.length ? ` — ORPHANED: ${report.orphaned.join(', ')}` : '';
|
|
35
33
|
console.log(`[catalog] reconcile: +${report.added.length} added, ${report.orphaned.length} orphaned, ` +
|
|
36
34
|
`${report.reactivated.length} reactivated${orphanNote}`);
|
|
37
35
|
return report;
|
|
@@ -35,6 +35,14 @@ export declare function expandGrantKeys(rawKeys: string[], allKeys: string[]): s
|
|
|
35
35
|
* integer against the BIGINT `mp_effective_*_ms` columns (timezone-immune).
|
|
36
36
|
*/
|
|
37
37
|
export declare function readRoleIdsForTenant(pool: Pool, principalId: string, tenantId: string, now?: () => number): Promise<string[]>;
|
|
38
|
+
/**
|
|
39
|
+
* The next instant (epoch-ms, strictly after `now`) at which this USER's in-effect roles in
|
|
40
|
+
* `tenantId` change by themselves — the earliest future `mp_effective_from_ms` (a posting starts)
|
|
41
|
+
* or `mp_effective_until_ms` (a posting ends) of the active membership. No version bump marks
|
|
42
|
+
* these moments, so cached facts must expire there (ADR-0035). `null` when there is none, and for
|
|
43
|
+
* a service account (its roles have no windows). A query failure propagates (fail closed).
|
|
44
|
+
*/
|
|
45
|
+
export declare function readNextPostingBoundaryMs(pool: Pool, principalId: string, tenantId: string, now?: () => number): Promise<number | null>;
|
|
38
46
|
export declare class DbPermissionFactsAdapter implements PermissionFactsPort {
|
|
39
47
|
private readonly pool;
|
|
40
48
|
private readonly registry;
|
|
@@ -52,6 +60,14 @@ export declare class DbPermissionFactsAdapter implements PermissionFactsPort {
|
|
|
52
60
|
*/
|
|
53
61
|
now?: () => number);
|
|
54
62
|
currentPermissionsVersion(_principalId: string, tenantId: string): Promise<number>;
|
|
63
|
+
/**
|
|
64
|
+
* The coarse grants + (ADR-0035) the LIVE role ids they were derived from + the time bound of
|
|
65
|
+
* those roles. The version is read FIRST, so a bump racing this load can only make the facts look
|
|
66
|
+
* older (one extra reload), never newer. `roleIds` is always reported (empty = genuinely none),
|
|
67
|
+
* so the session layer binds the in-effect roles, never the credential's `ic_roles`.
|
|
68
|
+
* `validUntilMs` = the next posting window boundary (absent when there is none); an outage on
|
|
69
|
+
* that read fails closed like every other read here.
|
|
70
|
+
*/
|
|
55
71
|
loadCoarseGrants(principalId: string, tenantId: string): Promise<CoarseGrants>;
|
|
56
72
|
private readVersion;
|
|
57
73
|
/**
|
|
@@ -1 +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;AA0CD;;;;;;;;;;;;GAYG;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,
|
|
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;AA0CD;;;;;;;;;;;;GAYG;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,CAqBnB;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB,GAC3B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkBxB;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;IAaxF;;;;;;;OAOG;IACG,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YAuBtE,WAAW;IASzB;;;OAGG;YACW,WAAW;YAIX,aAAa;CAS5B"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DbPermissionFactsAdapter = void 0;
|
|
4
4
|
exports.expandGrantKeys = expandGrantKeys;
|
|
5
5
|
exports.readRoleIdsForTenant = readRoleIdsForTenant;
|
|
6
|
+
exports.readNextPostingBoundaryMs = readNextPostingBoundaryMs;
|
|
6
7
|
const core_1 = require("@astram/core");
|
|
7
8
|
const MODULE_PREFIX = 'identity';
|
|
8
9
|
/**
|
|
@@ -85,16 +86,44 @@ async function readRoleIdsForTenant(pool, principalId, tenantId, now = Date.now)
|
|
|
85
86
|
const saRoles = await readServiceAccountRoleIds(pool, principalId, tenantId);
|
|
86
87
|
if (saRoles !== null)
|
|
87
88
|
return saRoles;
|
|
88
|
-
// Human path: roles from the active membership's in-effect postings
|
|
89
|
+
// Human path: roles from the active membership's in-effect postings — of an ACTIVE account only
|
|
90
|
+
// (security hardening Phase 3: a disabled user holds no roles, so gate 5 and every role-keyed
|
|
91
|
+
// path deny at once; `company_membership.cm_user_uuid` is an FK to `users`, so a member always
|
|
92
|
+
// has a users row).
|
|
89
93
|
const nowMs = now();
|
|
90
94
|
const rows = (await pool.query(`SELECT DISTINCT mp.mp_role_uuid AS role_uuid
|
|
91
95
|
FROM \`shield_app\`.\`company_membership\` cm
|
|
96
|
+
JOIN \`shield_app\`.\`users\` u ON u.u_uuid = cm.cm_user_uuid AND u.u_status = 'active'
|
|
92
97
|
JOIN \`shield_app\`.\`membership_posting\` mp ON mp.mp_membership_uuid = cm.cm_uuid
|
|
93
98
|
WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ? AND cm.cm_is_active = 1
|
|
94
99
|
AND (mp.mp_effective_from_ms IS NULL OR mp.mp_effective_from_ms <= ?)
|
|
95
100
|
AND (mp.mp_effective_until_ms IS NULL OR mp.mp_effective_until_ms > ?)`, [principalId, tenantId, nowMs, nowMs]));
|
|
96
101
|
return rows.map((r) => r.role_uuid);
|
|
97
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* The next instant (epoch-ms, strictly after `now`) at which this USER's in-effect roles in
|
|
105
|
+
* `tenantId` change by themselves — the earliest future `mp_effective_from_ms` (a posting starts)
|
|
106
|
+
* or `mp_effective_until_ms` (a posting ends) of the active membership. No version bump marks
|
|
107
|
+
* these moments, so cached facts must expire there (ADR-0035). `null` when there is none, and for
|
|
108
|
+
* a service account (its roles have no windows). A query failure propagates (fail closed).
|
|
109
|
+
*/
|
|
110
|
+
async function readNextPostingBoundaryMs(pool, principalId, tenantId, now = Date.now) {
|
|
111
|
+
const nowMs = now();
|
|
112
|
+
const rows = (await pool.query(`SELECT MIN(CASE
|
|
113
|
+
WHEN mp.mp_effective_from_ms IS NOT NULL AND mp.mp_effective_from_ms > ?
|
|
114
|
+
THEN mp.mp_effective_from_ms
|
|
115
|
+
WHEN mp.mp_effective_until_ms IS NOT NULL AND mp.mp_effective_until_ms > ?
|
|
116
|
+
THEN mp.mp_effective_until_ms
|
|
117
|
+
END) AS next_boundary_ms
|
|
118
|
+
FROM \`shield_app\`.\`company_membership\` cm
|
|
119
|
+
JOIN \`shield_app\`.\`membership_posting\` mp ON mp.mp_membership_uuid = cm.cm_uuid
|
|
120
|
+
WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ? AND cm.cm_is_active = 1`, [nowMs, nowMs, principalId, tenantId]));
|
|
121
|
+
const raw = rows[0]?.next_boundary_ms;
|
|
122
|
+
if (raw === null || raw === undefined)
|
|
123
|
+
return null;
|
|
124
|
+
const ms = Number(raw);
|
|
125
|
+
return Number.isFinite(ms) && ms > nowMs ? ms : null;
|
|
126
|
+
}
|
|
98
127
|
class DbPermissionFactsAdapter {
|
|
99
128
|
pool;
|
|
100
129
|
registry;
|
|
@@ -120,14 +149,24 @@ class DbPermissionFactsAdapter {
|
|
|
120
149
|
throw new core_1.AuthorizationUnavailableError('permission version store unavailable', cause);
|
|
121
150
|
}
|
|
122
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* The coarse grants + (ADR-0035) the LIVE role ids they were derived from + the time bound of
|
|
154
|
+
* those roles. The version is read FIRST, so a bump racing this load can only make the facts look
|
|
155
|
+
* older (one extra reload), never newer. `roleIds` is always reported (empty = genuinely none),
|
|
156
|
+
* so the session layer binds the in-effect roles, never the credential's `ic_roles`.
|
|
157
|
+
* `validUntilMs` = the next posting window boundary (absent when there is none); an outage on
|
|
158
|
+
* that read fails closed like every other read here.
|
|
159
|
+
*/
|
|
123
160
|
async loadCoarseGrants(principalId, tenantId) {
|
|
124
161
|
let version;
|
|
125
162
|
let roleIds;
|
|
126
163
|
let rawKeys;
|
|
164
|
+
let validUntilMs;
|
|
127
165
|
try {
|
|
128
166
|
version = await this.readVersion(tenantId);
|
|
129
167
|
roleIds = await this.readRoleIds(principalId, tenantId);
|
|
130
168
|
rawKeys = roleIds.length === 0 ? [] : await this.readGrantKeys(roleIds);
|
|
169
|
+
validUntilMs = await readNextPostingBoundaryMs(this.pool, principalId, tenantId, this.now);
|
|
131
170
|
}
|
|
132
171
|
catch (cause) {
|
|
133
172
|
if (cause instanceof core_1.AuthorizationUnavailableError)
|
|
@@ -135,7 +174,12 @@ class DbPermissionFactsAdapter {
|
|
|
135
174
|
throw new core_1.AuthorizationUnavailableError('permission facts store unavailable', cause);
|
|
136
175
|
}
|
|
137
176
|
const allKeys = this.registry.listActions().map((a) => a.permissionKey);
|
|
138
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
permissionsVersion: version,
|
|
179
|
+
grantedPermissionKeys: expandGrantKeys(rawKeys, allKeys),
|
|
180
|
+
roleIds,
|
|
181
|
+
...(validUntilMs !== null ? { validUntilMs } : {}),
|
|
182
|
+
};
|
|
139
183
|
}
|
|
140
184
|
async readVersion(tenantId) {
|
|
141
185
|
const rows = (await this.pool.query(`SELECT apv_version FROM \`shield_authz\`.\`authz_permission_version\`
|
|
@@ -13,8 +13,11 @@ interface AuthedRequest {
|
|
|
13
13
|
* grant; a non-root caller needs the grant. (`role_permission` itself is not a
|
|
14
14
|
* registered object type — it is administered THROUGH `role`, never classified directly.)
|
|
15
15
|
*
|
|
16
|
-
* Mutating routes pass `req.principal
|
|
17
|
-
*
|
|
16
|
+
* Mutating routes pass `req.principal` as the actor: it stamps who granted what (surfaced by the
|
|
17
|
+
* grants-summary listing as "by <name>"), and grant AND revoke run the role-binding rule (a role
|
|
18
|
+
* also used outside the caller's subtree is root-only) and the ADR-0029 ceiling ("only grant or
|
|
19
|
+
* revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling and
|
|
20
|
+
* binding → 403, a role outside the caller's subtree → 404, unknown key/bundle → 400.
|
|
18
21
|
*/
|
|
19
22
|
export declare class GrantAdminController {
|
|
20
23
|
private readonly grants;
|
|
@@ -25,7 +28,7 @@ export declare class GrantAdminController {
|
|
|
25
28
|
* grantor NAME (resolved server-side — no raw uuid). Gated `role:read`.
|
|
26
29
|
*
|
|
27
30
|
* ⚠️ Path is `access/role-grants` (NOT under `roles/...`) on purpose: a `roles/<x>` path
|
|
28
|
-
* would be captured by
|
|
31
|
+
* would be captured by RoleController's `@Get('roles/:uuid')` (registered first),
|
|
29
32
|
* routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
|
|
30
33
|
*/
|
|
31
34
|
grantsSummary(req: AuthedRequest, query: {
|
|
@@ -63,7 +66,7 @@ export declare class GrantAdminController {
|
|
|
63
66
|
success: boolean;
|
|
64
67
|
}>;
|
|
65
68
|
/** Revoke one permission key from a role. `:key` is URL-encoded (contains ':'). */
|
|
66
|
-
revoke(uuid: string, key: string): Promise<{
|
|
69
|
+
revoke(req: AuthedRequest, uuid: string, key: string): Promise<{
|
|
67
70
|
success: boolean;
|
|
68
71
|
}>;
|
|
69
72
|
/**
|
|
@@ -89,7 +92,7 @@ export declare class GrantAdminController {
|
|
|
89
92
|
* `role_permission` (see design §5.1 caveat re: shared keys). `:key` is the bundle slug.
|
|
90
93
|
* Unknown bundle → 400. Gated `role:update`.
|
|
91
94
|
*/
|
|
92
|
-
revokeBundle(uuid: string, key: string): Promise<{
|
|
95
|
+
revokeBundle(req: AuthedRequest, uuid: string, key: string): Promise<{
|
|
93
96
|
success: boolean;
|
|
94
97
|
}>;
|
|
95
98
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant-admin.controller.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"grant-admin.controller.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.controller.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAe,MAAM,0BAA0B,CAAC;AAI1E,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAaD;;;;;;;;;;;;;;GAcG;AACH,qBAEa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,iBAAiB;IAEtD;;;;;;;;OAQG;IAGG,aAAa,CACV,GAAG,EAAE,aAAa,EAEzB,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;;;IAcH,sDAAsD;IAGhD,OAAO;;;;;;;;;;IAIb;kGAC8F;IAGxF,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IAIjE,0CAA0C;IAGpC,KAAK,CACF,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAahC,mFAAmF;IAG7E,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;IAO9F;;;;OAIG;IAGG,OAAO;;;IAIb;;;;OAIG;IAGG,WAAW,CACR,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAahC;;;;OAIG;IAGG,YAAY,CACT,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACb,GAAG,EAAE,MAAM;;;CAU5B"}
|
|
@@ -19,7 +19,7 @@ exports.GrantAdminController = void 0;
|
|
|
19
19
|
const common_1 = require("@nestjs/common");
|
|
20
20
|
const guard_1 = require("@astram/guard");
|
|
21
21
|
const grant_admin_service_js_1 = require("./grant-admin.service.js");
|
|
22
|
-
const
|
|
22
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
23
23
|
/**
|
|
24
24
|
* Derive the caller's subtree scope from the resolved principal (Task 4.2). Uses the SAME cached
|
|
25
25
|
* `scopeSubtreeNodeIds` the RecordEngine's list-scope uses (one source of truth). Fail-safe to the
|
|
@@ -39,8 +39,11 @@ function subtreeScopeOf(req) {
|
|
|
39
39
|
* grant; a non-root caller needs the grant. (`role_permission` itself is not a
|
|
40
40
|
* registered object type — it is administered THROUGH `role`, never classified directly.)
|
|
41
41
|
*
|
|
42
|
-
* Mutating routes pass `req.principal
|
|
43
|
-
*
|
|
42
|
+
* Mutating routes pass `req.principal` as the actor: it stamps who granted what (surfaced by the
|
|
43
|
+
* grants-summary listing as "by <name>"), and grant AND revoke run the role-binding rule (a role
|
|
44
|
+
* also used outside the caller's subtree is root-only) and the ADR-0029 ceiling ("only grant or
|
|
45
|
+
* revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling and
|
|
46
|
+
* binding → 403, a role outside the caller's subtree → 404, unknown key/bundle → 400.
|
|
44
47
|
*/
|
|
45
48
|
let GrantAdminController = class GrantAdminController {
|
|
46
49
|
grants;
|
|
@@ -53,7 +56,7 @@ let GrantAdminController = class GrantAdminController {
|
|
|
53
56
|
* grantor NAME (resolved server-side — no raw uuid). Gated `role:read`.
|
|
54
57
|
*
|
|
55
58
|
* ⚠️ Path is `access/role-grants` (NOT under `roles/...`) on purpose: a `roles/<x>` path
|
|
56
|
-
* would be captured by
|
|
59
|
+
* would be captured by RoleController's `@Get('roles/:uuid')` (registered first),
|
|
57
60
|
* routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
|
|
58
61
|
*/
|
|
59
62
|
async grantsSummary(req, query) {
|
|
@@ -92,15 +95,13 @@ let GrantAdminController = class GrantAdminController {
|
|
|
92
95
|
catch (e) {
|
|
93
96
|
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
94
97
|
throw new common_1.BadRequestException(e.message);
|
|
95
|
-
if (e instanceof grant_ceiling_js_1.GrantCeilingError)
|
|
96
|
-
throw new common_1.ForbiddenException(e.message);
|
|
97
98
|
throw e;
|
|
98
99
|
}
|
|
99
100
|
return { success: true };
|
|
100
101
|
}
|
|
101
102
|
/** Revoke one permission key from a role. `:key` is URL-encoded (contains ':'). */
|
|
102
|
-
async revoke(uuid, key) {
|
|
103
|
-
await this.grants.revoke(uuid, decodeURIComponent(key));
|
|
103
|
+
async revoke(req, uuid, key) {
|
|
104
|
+
await this.grants.revoke(uuid, decodeURIComponent(key), req.principal);
|
|
104
105
|
return { success: true };
|
|
105
106
|
}
|
|
106
107
|
// ── Access bundles ────────────────────────────────────────────────────────
|
|
@@ -127,8 +128,6 @@ let GrantAdminController = class GrantAdminController {
|
|
|
127
128
|
catch (e) {
|
|
128
129
|
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
129
130
|
throw new common_1.BadRequestException(e.message);
|
|
130
|
-
if (e instanceof grant_ceiling_js_1.GrantCeilingError)
|
|
131
|
-
throw new common_1.ForbiddenException(e.message);
|
|
132
131
|
throw e;
|
|
133
132
|
}
|
|
134
133
|
return { success: true };
|
|
@@ -138,9 +137,9 @@ let GrantAdminController = class GrantAdminController {
|
|
|
138
137
|
* `role_permission` (see design §5.1 caveat re: shared keys). `:key` is the bundle slug.
|
|
139
138
|
* Unknown bundle → 400. Gated `role:update`.
|
|
140
139
|
*/
|
|
141
|
-
async revokeBundle(uuid, key) {
|
|
140
|
+
async revokeBundle(req, uuid, key) {
|
|
142
141
|
try {
|
|
143
|
-
await this.grants.revokeBundle(uuid, decodeURIComponent(key));
|
|
142
|
+
await this.grants.revokeBundle(uuid, decodeURIComponent(key), req.principal);
|
|
144
143
|
}
|
|
145
144
|
catch (e) {
|
|
146
145
|
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
@@ -189,10 +188,11 @@ __decorate([
|
|
|
189
188
|
__decorate([
|
|
190
189
|
(0, common_1.Delete)('roles/:uuid/permissions/:key'),
|
|
191
190
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
192
|
-
__param(0, (0, common_1.
|
|
193
|
-
__param(1, (0, common_1.Param)('
|
|
191
|
+
__param(0, (0, common_1.Req)()),
|
|
192
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
193
|
+
__param(2, (0, common_1.Param)('key')),
|
|
194
194
|
__metadata("design:type", Function),
|
|
195
|
-
__metadata("design:paramtypes", [String, String]),
|
|
195
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
196
196
|
__metadata("design:returntype", Promise)
|
|
197
197
|
], GrantAdminController.prototype, "revoke", null);
|
|
198
198
|
__decorate([
|
|
@@ -215,13 +215,15 @@ __decorate([
|
|
|
215
215
|
__decorate([
|
|
216
216
|
(0, common_1.Delete)('roles/:uuid/access-bundles/:key'),
|
|
217
217
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
218
|
-
__param(0, (0, common_1.
|
|
219
|
-
__param(1, (0, common_1.Param)('
|
|
218
|
+
__param(0, (0, common_1.Req)()),
|
|
219
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
220
|
+
__param(2, (0, common_1.Param)('key')),
|
|
220
221
|
__metadata("design:type", Function),
|
|
221
|
-
__metadata("design:paramtypes", [String, String]),
|
|
222
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
222
223
|
__metadata("design:returntype", Promise)
|
|
223
224
|
], GrantAdminController.prototype, "revokeBundle", null);
|
|
224
225
|
exports.GrantAdminController = GrantAdminController = __decorate([
|
|
225
226
|
(0, common_1.Controller)('identity'),
|
|
227
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
226
228
|
__metadata("design:paramtypes", [grant_admin_service_js_1.GrantAdminService])
|
|
227
229
|
], GrantAdminController);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Pool } from 'mariadb';
|
|
2
2
|
import type { Principal } from '@astram/core';
|
|
3
|
+
import type { AuditPort } from '@astram/engine';
|
|
3
4
|
import { PermissionVersionBumper } from './permission-version.bumper.js';
|
|
4
5
|
import { AccessBundleStore } from './access-bundle-store.js';
|
|
5
|
-
import
|
|
6
|
+
import { GrantCeiling } from './grant-ceiling.js';
|
|
6
7
|
/**
|
|
7
8
|
* The caller's reachable subtree, threaded from `req.principal` (Task 4.2). It scopes the
|
|
8
9
|
* grant-admin reads so a caller sees ONLY roles bound within their subtree — the SAME cached
|
|
@@ -52,14 +53,18 @@ export declare class GrantAdminService {
|
|
|
52
53
|
private readonly pool;
|
|
53
54
|
private readonly bumper;
|
|
54
55
|
private readonly ceiling?;
|
|
56
|
+
private readonly audit?;
|
|
55
57
|
private readonly bundles;
|
|
56
58
|
/**
|
|
57
|
-
* @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold")
|
|
58
|
-
*
|
|
59
|
-
* (legacy wiring / tests that don't
|
|
60
|
-
* identity module ALWAYS wires it, so production always
|
|
59
|
+
* @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold") plus the
|
|
60
|
+
* security-hardening role-binding rule. Enforced on grant/revoke/grantBundle/revokeBundle for a
|
|
61
|
+
* non-root actor. OPTIONAL for back-compat: when absent (legacy wiring / tests that don't
|
|
62
|
+
* exercise it), neither check runs — the identity module ALWAYS wires it, so production always
|
|
63
|
+
* enforces. Root bypasses regardless.
|
|
64
|
+
* @param audit The grant-administration audit sink (D6): written on the grant's own transaction
|
|
65
|
+
* connection, fail-closed. The identity module ALWAYS wires the engine `MariaDbAuditSink`.
|
|
61
66
|
*/
|
|
62
|
-
constructor(pool: Pool, bumper: PermissionVersionBumper, bundleStore?: AccessBundleStore, ceiling?: GrantCeiling | undefined);
|
|
67
|
+
constructor(pool: Pool, bumper: PermissionVersionBumper, bundleStore?: AccessBundleStore, ceiling?: GrantCeiling | undefined, audit?: AuditPort | undefined);
|
|
63
68
|
/** All grantable exact keys (from the catalog) + the computed wildcard set. */
|
|
64
69
|
listCatalog(): Promise<{
|
|
65
70
|
catalog: CatalogRow[];
|
|
@@ -76,15 +81,65 @@ export declare class GrantAdminService {
|
|
|
76
81
|
/** Validate the key against the catalog + wildcard set. Throws BadKeyError if unknown. */
|
|
77
82
|
private assertValidKey;
|
|
78
83
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
+
* Security hardening #1 (D1 interim rule) + ADR-0029, in this order, BEFORE any write: the role
|
|
85
|
+
* must be administrable by the actor (bound inside its subtree and nowhere outside — a shared
|
|
86
|
+
* role is root-only), then every key must be covered by the actor's own effective keys ("only
|
|
87
|
+
* grant — or revoke — what you hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒
|
|
88
|
+
* neither check runs; the identity module always wires it.
|
|
89
|
+
*/
|
|
90
|
+
private assertMayChangeGrants;
|
|
91
|
+
/**
|
|
92
|
+
* Run one grant-administration write in ONE transaction: the `role_permission` statements, the
|
|
93
|
+
* permission-version bump of every tenant using the role, and the audit row(s) (D6 — audited in
|
|
94
|
+
* the SAME transaction, fail-closed: an audit failure rolls the grant back). Store failures map
|
|
95
|
+
* to AuthorizationUnavailableError (503).
|
|
96
|
+
*/
|
|
97
|
+
private writeGrants;
|
|
98
|
+
/**
|
|
99
|
+
* Write the grant-administration audit row(s) on the transaction connection (D6). One row per
|
|
100
|
+
* distinct tenant among the actor's tenant and every tenant the change bumped, so each affected
|
|
101
|
+
* tenant's auditor sees it. Names, never ids, in the detail (ADR-0012): the role name and the
|
|
102
|
+
* bumped tenants' names; the viewer resolves the actor and row tenant itself. No sink wired
|
|
103
|
+
* (legacy/tests) ⇒ nothing is written.
|
|
104
|
+
*/
|
|
105
|
+
private auditGrantChange;
|
|
106
|
+
/**
|
|
107
|
+
* Grant a key to a role (idempotent) + bump the version + audit, atomically. The `grantor`
|
|
108
|
+
* Principal stamps `rp_granted_at`/`rp_granted_by` on a NEWLY-inserted row (a duplicate is a
|
|
109
|
+
* no-op and its existing stamp is left as-is). Order: catalog validation (400) → role binding
|
|
110
|
+
* (404/403) → ceiling (403) → write. Root bypasses the binding rule and the ceiling.
|
|
84
111
|
*/
|
|
85
112
|
grant(roleUuid: string, key: string, grantor: Principal): Promise<void>;
|
|
86
|
-
/**
|
|
87
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Revoke a key from a role (no-op if absent) + bump + audit, atomically. Security hardening #1:
|
|
115
|
+
* the actor must be able to administer the role (binding rule) and must hold the key itself —
|
|
116
|
+
* "only revoke what you hold" — so a delegated admin can no longer strip a role of authority it
|
|
117
|
+
* never had. Phase 8 #2: it must also dominate every user holding the role (checked under the
|
|
118
|
+
* role lock in `writeGrants`). Root bypasses.
|
|
119
|
+
*/
|
|
120
|
+
revoke(roleUuid: string, key: string, actor: Principal): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Remove a role that is being DELETED from authorization (Phase 8 #2 follow-up): every
|
|
123
|
+
* `role_permission` row of the role, and the role from every service account's `sa_role_ids`
|
|
124
|
+
* (guard's API-key store reads that column live, so a stale entry would keep the deleted role's
|
|
125
|
+
* keys on the agent). ONE transaction on the shared authz pool — the same pool, bump, audit and
|
|
126
|
+
* error mapping as {@link revoke}:
|
|
127
|
+
* 1. lock the holding accounts (`FOR UPDATE`), then check the actor may strip the role from
|
|
128
|
+
* each (`GrantCeiling.assertMayStripRoleFromServiceAccounts`, root bypasses);
|
|
129
|
+
* 2. bump every tenant using the role — postings AND the accounts' tenants — before the strip,
|
|
130
|
+
* so the account tenants are still found;
|
|
131
|
+
* 3. delete the role's grants; rewrite each account's role list without it (edit stamp moved
|
|
132
|
+
* forward, so a concurrent preserve-merge edit conflicts instead of re-adding it);
|
|
133
|
+
* 4. audit as `role.permission_revoked` (`roleDeleted: true`), commit, publish the versions.
|
|
134
|
+
* Returns the bumped tenants.
|
|
135
|
+
*
|
|
136
|
+
* The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection and
|
|
137
|
+
* has already run the binding rule and user-holder dominance; this method therefore does NOT
|
|
138
|
+
* lock the role row (that would wait on the caller). It commits BEFORE the caller deletes the
|
|
139
|
+
* role, so a failure in between leaves a role with no grants and no agents: fewer permissions,
|
|
140
|
+
* never live grants for a deleted role (fail closed).
|
|
141
|
+
*/
|
|
142
|
+
purgeRole(roleUuid: string, actor: Principal): Promise<string[]>;
|
|
88
143
|
/** All active bundles + their resolved atomic keys, for the Access UI. */
|
|
89
144
|
listBundles(): Promise<{
|
|
90
145
|
bundles: BundleView[];
|
|
@@ -99,11 +154,12 @@ export declare class GrantAdminService {
|
|
|
99
154
|
grantBundle(roleUuid: string, bundleKey: string, grantor: Principal): Promise<void>;
|
|
100
155
|
/**
|
|
101
156
|
* Revoke a bundle from a role: resolve → DELETE each of the bundle's atomic keys, in
|
|
102
|
-
* ONE transaction with ONE version bump. Caveat (design §5.1): this removes exactly the
|
|
157
|
+
* ONE transaction with ONE version bump + audit. Caveat (design §5.1): this removes exactly the
|
|
103
158
|
* bundle's keys even if a manual grant or another bundle shares one — the model is
|
|
104
|
-
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`.
|
|
159
|
+
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`. Security hardening
|
|
160
|
+
* #1: the binding rule + "only revoke what you hold" apply (root bypasses).
|
|
105
161
|
*/
|
|
106
|
-
revokeBundle(roleUuid: string, bundleKey: string): Promise<void>;
|
|
162
|
+
revokeBundle(roleUuid: string, bundleKey: string, actor: Principal): Promise<void>;
|
|
107
163
|
/** Resolve a bundle key → its atomic keys; throw BadKeyError when unknown/orphaned. */
|
|
108
164
|
private resolveBundleKeysOrThrow;
|
|
109
165
|
/**
|