@astram/identity 0.3.1 → 0.5.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 +24 -4
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
- package/dist/features/authz/db-permission-facts.adapter.js +92 -5
- 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 +22 -15
- package/dist/features/authz/grant-admin.service.d.ts +57 -11
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +163 -85
- package/dist/features/authz/grant-ceiling.d.ts +111 -0
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -0
- package/dist/features/authz/grant-ceiling.js +248 -0
- 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 +45 -1
- package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
- package/dist/features/authz/permission-version.bumper.js +106 -16
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- 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 +46 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
- package/dist/features/company/company-hierarchy.service.js +100 -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.map +1 -1
- package/dist/features/identity-feature.module.js +123 -6
- 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 +5 -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 +16 -8
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +24 -10
- package/dist/features/scope/scope-closure.service.d.ts +44 -4
- package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
- package/dist/features/scope/scope-closure.service.js +215 -82
- package/dist/features/service-account/service-account.controller.d.ts +63 -0
- package/dist/features/service-account/service-account.controller.d.ts.map +1 -0
- package/dist/features/service-account/service-account.controller.js +295 -0
- package/dist/features/service-account/service-account.service.d.ts +209 -0
- package/dist/features/service-account/service-account.service.d.ts.map +1 -0
- package/dist/features/service-account/service-account.service.js +528 -0
- package/dist/features/service-account/service-account.store.d.ts +91 -0
- package/dist/features/service-account/service-account.store.d.ts.map +1 -0
- package/dist/features/service-account/service-account.store.js +158 -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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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 +14 -3
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +3 -0
- package/dist/service-account.object.d.ts +31 -0
- package/dist/service-account.object.d.ts.map +1 -0
- package/dist/service-account.object.js +75 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +3 -0
- package/migrations/1710000000018-CreateServiceAccount.cjs +53 -0
- package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,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;
|
|
@@ -22,15 +22,27 @@ import type { PermissionFactsPort, CoarseGrants, ModuleRegistry } from '@astram/
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function expandGrantKeys(rawKeys: string[], allKeys: string[]): string[];
|
|
24
24
|
/**
|
|
25
|
-
* The principal's role uuids
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* The principal's role uuids in `tenantId` — the SINGLE source of "what roles does this principal
|
|
26
|
+
* hold here" used by the object-type gate (gate 5), the grant-ceiling (ADR-0029), and the
|
|
27
|
+
* cross-tenant grant authorize. KIND-AWARE (ADR-0033): a SERVICE ACCOUNT's roles come from its
|
|
28
|
+
* `service_account.sa_role_ids`; a USER's roles come from its ACTIVE membership's in-effect
|
|
29
|
+
* postings. The service-account lookup runs first — a hit short-circuits (a service-account id
|
|
30
|
+
* never has membership rows anyway); a miss falls through to the timezone-safe epoch-ms membership
|
|
31
|
+
* window read. Making THIS function kind-aware gives all three callers correct grants uniformly
|
|
32
|
+
* (one definition of "a principal's roles"), preserving the single `decide()` path.
|
|
29
33
|
*
|
|
30
34
|
* @param now injectable clock (epoch-ms), defaults to `Date.now` — bound as a `?`
|
|
31
35
|
* integer against the BIGINT `mp_effective_*_ms` columns (timezone-immune).
|
|
32
36
|
*/
|
|
33
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>;
|
|
34
46
|
export declare class DbPermissionFactsAdapter implements PermissionFactsPort {
|
|
35
47
|
private readonly pool;
|
|
36
48
|
private readonly registry;
|
|
@@ -48,6 +60,14 @@ export declare class DbPermissionFactsAdapter implements PermissionFactsPort {
|
|
|
48
60
|
*/
|
|
49
61
|
now?: () => number);
|
|
50
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
|
+
*/
|
|
51
71
|
loadCoarseGrants(principalId: string, tenantId: string): Promise<CoarseGrants>;
|
|
52
72
|
private readVersion;
|
|
53
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;
|
|
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
|
/**
|
|
@@ -33,25 +34,96 @@ function expandGrantKeys(rawKeys, allKeys) {
|
|
|
33
34
|
}
|
|
34
35
|
return [...out];
|
|
35
36
|
}
|
|
37
|
+
/** Parse a `sa_role_ids` JSON column (driver returns it already-parsed OR as a string). */
|
|
38
|
+
function parseRoleIdsJson(v) {
|
|
39
|
+
const arr = Array.isArray(v)
|
|
40
|
+
? v
|
|
41
|
+
: typeof v === 'string'
|
|
42
|
+
? (() => {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(v);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
})()
|
|
50
|
+
: [];
|
|
51
|
+
return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string') : [];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A SERVICE ACCOUNT (ADR-0033) holds its roles directly on `service_account.sa_role_ids` — it has
|
|
55
|
+
* NO `company_membership`/`membership_posting` rows (those model a human's org placement). So its
|
|
56
|
+
* roles are read from the account row: an ACTIVE account in this tenant → its `sa_role_ids`. Returns
|
|
57
|
+
* `null` when `principalId` is not a service account here (→ the caller falls through to the human
|
|
58
|
+
* membership path). A DISABLED account returns `[]` (in-scope but no roles → deny everything —
|
|
59
|
+
* defense-in-depth alongside the key's own revoke check).
|
|
60
|
+
*/
|
|
61
|
+
async function readServiceAccountRoleIds(pool, principalId, tenantId) {
|
|
62
|
+
const rows = (await pool.query(`SELECT sa_role_ids, sa_status FROM \`shield_app\`.\`service_account\`
|
|
63
|
+
WHERE sa_id = ? AND sa_tenant_id = ? LIMIT 1`, [principalId, tenantId]));
|
|
64
|
+
const row = rows[0];
|
|
65
|
+
if (row === undefined)
|
|
66
|
+
return null; // not a service account here → let the user path run.
|
|
67
|
+
if (row.sa_status !== 'active')
|
|
68
|
+
return []; // disabled → no roles (deny-by-default).
|
|
69
|
+
return parseRoleIdsJson(row.sa_role_ids);
|
|
70
|
+
}
|
|
36
71
|
/**
|
|
37
|
-
* The principal's role uuids
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
72
|
+
* The principal's role uuids in `tenantId` — the SINGLE source of "what roles does this principal
|
|
73
|
+
* hold here" used by the object-type gate (gate 5), the grant-ceiling (ADR-0029), and the
|
|
74
|
+
* cross-tenant grant authorize. KIND-AWARE (ADR-0033): a SERVICE ACCOUNT's roles come from its
|
|
75
|
+
* `service_account.sa_role_ids`; a USER's roles come from its ACTIVE membership's in-effect
|
|
76
|
+
* postings. The service-account lookup runs first — a hit short-circuits (a service-account id
|
|
77
|
+
* never has membership rows anyway); a miss falls through to the timezone-safe epoch-ms membership
|
|
78
|
+
* window read. Making THIS function kind-aware gives all three callers correct grants uniformly
|
|
79
|
+
* (one definition of "a principal's roles"), preserving the single `decide()` path.
|
|
41
80
|
*
|
|
42
81
|
* @param now injectable clock (epoch-ms), defaults to `Date.now` — bound as a `?`
|
|
43
82
|
* integer against the BIGINT `mp_effective_*_ms` columns (timezone-immune).
|
|
44
83
|
*/
|
|
45
84
|
async function readRoleIdsForTenant(pool, principalId, tenantId, now = Date.now) {
|
|
85
|
+
// Service-account path (ADR-0033): its roles live on the account row, not memberships.
|
|
86
|
+
const saRoles = await readServiceAccountRoleIds(pool, principalId, tenantId);
|
|
87
|
+
if (saRoles !== null)
|
|
88
|
+
return saRoles;
|
|
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).
|
|
46
93
|
const nowMs = now();
|
|
47
94
|
const rows = (await pool.query(`SELECT DISTINCT mp.mp_role_uuid AS role_uuid
|
|
48
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'
|
|
49
97
|
JOIN \`shield_app\`.\`membership_posting\` mp ON mp.mp_membership_uuid = cm.cm_uuid
|
|
50
98
|
WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ? AND cm.cm_is_active = 1
|
|
51
99
|
AND (mp.mp_effective_from_ms IS NULL OR mp.mp_effective_from_ms <= ?)
|
|
52
100
|
AND (mp.mp_effective_until_ms IS NULL OR mp.mp_effective_until_ms > ?)`, [principalId, tenantId, nowMs, nowMs]));
|
|
53
101
|
return rows.map((r) => r.role_uuid);
|
|
54
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
|
+
}
|
|
55
127
|
class DbPermissionFactsAdapter {
|
|
56
128
|
pool;
|
|
57
129
|
registry;
|
|
@@ -77,14 +149,24 @@ class DbPermissionFactsAdapter {
|
|
|
77
149
|
throw new core_1.AuthorizationUnavailableError('permission version store unavailable', cause);
|
|
78
150
|
}
|
|
79
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
|
+
*/
|
|
80
160
|
async loadCoarseGrants(principalId, tenantId) {
|
|
81
161
|
let version;
|
|
82
162
|
let roleIds;
|
|
83
163
|
let rawKeys;
|
|
164
|
+
let validUntilMs;
|
|
84
165
|
try {
|
|
85
166
|
version = await this.readVersion(tenantId);
|
|
86
167
|
roleIds = await this.readRoleIds(principalId, tenantId);
|
|
87
168
|
rawKeys = roleIds.length === 0 ? [] : await this.readGrantKeys(roleIds);
|
|
169
|
+
validUntilMs = await readNextPostingBoundaryMs(this.pool, principalId, tenantId, this.now);
|
|
88
170
|
}
|
|
89
171
|
catch (cause) {
|
|
90
172
|
if (cause instanceof core_1.AuthorizationUnavailableError)
|
|
@@ -92,7 +174,12 @@ class DbPermissionFactsAdapter {
|
|
|
92
174
|
throw new core_1.AuthorizationUnavailableError('permission facts store unavailable', cause);
|
|
93
175
|
}
|
|
94
176
|
const allKeys = this.registry.listActions().map((a) => a.permissionKey);
|
|
95
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
permissionsVersion: version,
|
|
179
|
+
grantedPermissionKeys: expandGrantKeys(rawKeys, allKeys),
|
|
180
|
+
roleIds,
|
|
181
|
+
...(validUntilMs !== null ? { validUntilMs } : {}),
|
|
182
|
+
};
|
|
96
183
|
}
|
|
97
184
|
async readVersion(tenantId) {
|
|
98
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,6 +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 identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
22
23
|
/**
|
|
23
24
|
* Derive the caller's subtree scope from the resolved principal (Task 4.2). Uses the SAME cached
|
|
24
25
|
* `scopeSubtreeNodeIds` the RecordEngine's list-scope uses (one source of truth). Fail-safe to the
|
|
@@ -38,8 +39,11 @@ function subtreeScopeOf(req) {
|
|
|
38
39
|
* grant; a non-root caller needs the grant. (`role_permission` itself is not a
|
|
39
40
|
* registered object type — it is administered THROUGH `role`, never classified directly.)
|
|
40
41
|
*
|
|
41
|
-
* Mutating routes pass `req.principal
|
|
42
|
-
*
|
|
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.
|
|
43
47
|
*/
|
|
44
48
|
let GrantAdminController = class GrantAdminController {
|
|
45
49
|
grants;
|
|
@@ -52,7 +56,7 @@ let GrantAdminController = class GrantAdminController {
|
|
|
52
56
|
* grantor NAME (resolved server-side — no raw uuid). Gated `role:read`.
|
|
53
57
|
*
|
|
54
58
|
* ⚠️ Path is `access/role-grants` (NOT under `roles/...`) on purpose: a `roles/<x>` path
|
|
55
|
-
* would be captured by
|
|
59
|
+
* would be captured by RoleController's `@Get('roles/:uuid')` (registered first),
|
|
56
60
|
* routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
|
|
57
61
|
*/
|
|
58
62
|
async grantsSummary(req, query) {
|
|
@@ -86,7 +90,7 @@ let GrantAdminController = class GrantAdminController {
|
|
|
86
90
|
if (!key)
|
|
87
91
|
throw new common_1.BadRequestException('key required');
|
|
88
92
|
try {
|
|
89
|
-
await this.grants.grant(uuid, key, req.principal
|
|
93
|
+
await this.grants.grant(uuid, key, req.principal);
|
|
90
94
|
}
|
|
91
95
|
catch (e) {
|
|
92
96
|
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
@@ -96,8 +100,8 @@ let GrantAdminController = class GrantAdminController {
|
|
|
96
100
|
return { success: true };
|
|
97
101
|
}
|
|
98
102
|
/** Revoke one permission key from a role. `:key` is URL-encoded (contains ':'). */
|
|
99
|
-
async revoke(uuid, key) {
|
|
100
|
-
await this.grants.revoke(uuid, decodeURIComponent(key));
|
|
103
|
+
async revoke(req, uuid, key) {
|
|
104
|
+
await this.grants.revoke(uuid, decodeURIComponent(key), req.principal);
|
|
101
105
|
return { success: true };
|
|
102
106
|
}
|
|
103
107
|
// ── Access bundles ────────────────────────────────────────────────────────
|
|
@@ -119,7 +123,7 @@ let GrantAdminController = class GrantAdminController {
|
|
|
119
123
|
if (!key)
|
|
120
124
|
throw new common_1.BadRequestException('key required');
|
|
121
125
|
try {
|
|
122
|
-
await this.grants.grantBundle(uuid, key, req.principal
|
|
126
|
+
await this.grants.grantBundle(uuid, key, req.principal);
|
|
123
127
|
}
|
|
124
128
|
catch (e) {
|
|
125
129
|
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
@@ -133,9 +137,9 @@ let GrantAdminController = class GrantAdminController {
|
|
|
133
137
|
* `role_permission` (see design §5.1 caveat re: shared keys). `:key` is the bundle slug.
|
|
134
138
|
* Unknown bundle → 400. Gated `role:update`.
|
|
135
139
|
*/
|
|
136
|
-
async revokeBundle(uuid, key) {
|
|
140
|
+
async revokeBundle(req, uuid, key) {
|
|
137
141
|
try {
|
|
138
|
-
await this.grants.revokeBundle(uuid, decodeURIComponent(key));
|
|
142
|
+
await this.grants.revokeBundle(uuid, decodeURIComponent(key), req.principal);
|
|
139
143
|
}
|
|
140
144
|
catch (e) {
|
|
141
145
|
if (e instanceof grant_admin_service_js_1.BadKeyError)
|
|
@@ -184,10 +188,11 @@ __decorate([
|
|
|
184
188
|
__decorate([
|
|
185
189
|
(0, common_1.Delete)('roles/:uuid/permissions/:key'),
|
|
186
190
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
187
|
-
__param(0, (0, common_1.
|
|
188
|
-
__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')),
|
|
189
194
|
__metadata("design:type", Function),
|
|
190
|
-
__metadata("design:paramtypes", [String, String]),
|
|
195
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
191
196
|
__metadata("design:returntype", Promise)
|
|
192
197
|
], GrantAdminController.prototype, "revoke", null);
|
|
193
198
|
__decorate([
|
|
@@ -210,13 +215,15 @@ __decorate([
|
|
|
210
215
|
__decorate([
|
|
211
216
|
(0, common_1.Delete)('roles/:uuid/access-bundles/:key'),
|
|
212
217
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
213
|
-
__param(0, (0, common_1.
|
|
214
|
-
__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')),
|
|
215
221
|
__metadata("design:type", Function),
|
|
216
|
-
__metadata("design:paramtypes", [String, String]),
|
|
222
|
+
__metadata("design:paramtypes", [Object, String, String]),
|
|
217
223
|
__metadata("design:returntype", Promise)
|
|
218
224
|
], GrantAdminController.prototype, "revokeBundle", null);
|
|
219
225
|
exports.GrantAdminController = GrantAdminController = __decorate([
|
|
220
226
|
(0, common_1.Controller)('identity'),
|
|
227
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
221
228
|
__metadata("design:paramtypes", [grant_admin_service_js_1.GrantAdminService])
|
|
222
229
|
], GrantAdminController);
|