@astram/identity 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-facts-keys.js.map +1 -0
- package/dist/branch.object.js.map +1 -0
- package/dist/company-branch.object.js.map +1 -0
- package/dist/company-department.object.js.map +1 -0
- package/dist/company-membership.object.js.map +1 -0
- package/dist/company.object.js.map +1 -0
- package/dist/cross-tenant-grant.object.js.map +1 -0
- package/dist/department.object.js.map +1 -0
- package/dist/features/access/access-admin.access.js.map +1 -0
- package/dist/features/access/company-admin.access.js.map +1 -0
- package/dist/features/access/index.js.map +1 -0
- package/dist/features/access/user-management.access.js.map +1 -0
- package/dist/features/audit/audit-read.service.js.map +1 -0
- package/dist/features/audit/audit.controller.js.map +1 -0
- package/dist/features/audit/directory-label-resolver.js.map +1 -0
- package/dist/features/audit/mariadb-audit-reader.js.map +1 -0
- package/dist/features/authz/access-bundle-reconcile.job.js.map +1 -0
- package/dist/features/authz/access-bundle-store.js.map +1 -0
- package/dist/features/authz/catalog-bootstrap.provider.js.map +1 -0
- package/dist/features/authz/catalog-reconcile.job.js.map +1 -0
- package/dist/features/authz/db-permission-facts.adapter.d.ts +8 -4
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
- package/dist/features/authz/db-permission-facts.adapter.js +47 -4
- package/dist/features/authz/db-permission-facts.adapter.js.map +1 -0
- package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.controller.js +7 -2
- package/dist/features/authz/grant-admin.controller.js.map +1 -0
- package/dist/features/authz/grant-admin.service.d.ts +19 -6
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +25 -6
- package/dist/features/authz/grant-admin.service.js.map +1 -0
- package/dist/features/authz/grant-ceiling.d.ts +60 -0
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -0
- package/dist/features/authz/grant-ceiling.js +94 -0
- package/dist/features/authz/grant-ceiling.js.map +1 -0
- package/dist/features/authz/mariadb-catalog-store.js.map +1 -0
- package/dist/features/authz/permission-version.bumper.js.map +1 -0
- package/dist/features/authz/record-authorization.js.map +1 -0
- package/dist/features/authz/session-facts.reader.js.map +1 -0
- package/dist/features/company/company.controller.js.map +1 -0
- package/dist/features/company/company.service.js.map +1 -0
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.js.map +1 -0
- package/dist/features/cross-tenant/cross-tenant-grant.controller.js.map +1 -0
- package/dist/features/cross-tenant/grant-authorize.js.map +1 -0
- package/dist/features/identity-feature.module.d.ts +9 -1
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +78 -8
- package/dist/features/identity-feature.module.js.map +1 -0
- package/dist/features/membership/membership-checker.js.map +1 -0
- package/dist/features/membership/membership.controller.js.map +1 -0
- package/dist/features/membership/membership.service.js.map +1 -0
- package/dist/features/membership/posting-version-bump.js.map +1 -0
- package/dist/features/membership/posting.service.js.map +1 -0
- package/dist/features/org-structure/org-catalog.controller.js.map +1 -0
- package/dist/features/org-structure/org-catalog.service.js.map +1 -0
- package/dist/features/policy/policy.controller.js.map +1 -0
- package/dist/features/policy/policy.service.js.map +1 -0
- package/dist/features/role/role.controller.js.map +1 -0
- package/dist/features/role/role.service.js.map +1 -0
- package/dist/features/scope/scope-closure.service.js.map +1 -0
- package/dist/features/scope/scope-subtree.resolver.js.map +1 -0
- package/dist/features/service-account/service-account.controller.d.ts +62 -0
- package/dist/features/service-account/service-account.controller.d.ts.map +1 -0
- package/dist/features/service-account/service-account.controller.js +281 -0
- package/dist/features/service-account/service-account.service.d.ts +202 -0
- package/dist/features/service-account/service-account.service.d.ts.map +1 -0
- package/dist/features/service-account/service-account.service.js +483 -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.js.map +1 -0
- package/dist/features/share/share.service.js.map +1 -0
- package/dist/features/shared/http.js.map +1 -0
- package/dist/features/shared/list-query.js.map +1 -0
- package/dist/features/shared/named-option.js.map +1 -0
- package/dist/features/shared/record-resolver.js.map +1 -0
- package/dist/features/user/user.controller.js.map +1 -0
- package/dist/features/user/user.service.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -0
- package/dist/membership-posting.object.js.map +1 -0
- package/dist/migrations-dir.js.map +1 -0
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +2 -1
- package/dist/objects.js.map +1 -0
- package/dist/record-share.object.js.map +1 -0
- package/dist/role.object.js.map +1 -0
- package/dist/schema.js.map +1 -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/shared-resource.object.js.map +1 -0
- package/dist/user.object.js.map +1 -0
- package/migrations/1710000000018-CreateServiceAccount.cjs +53 -0
- package/package.json +4 -4
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ServiceAccountController = void 0;
|
|
16
|
+
/**
|
|
17
|
+
* ServiceAccountController — the Service Accounts admin API (ADR-0033). Seven routes over
|
|
18
|
+
* {@link ServiceAccountService}, all `@Classified({ typeKey: 'service_account', … })` so the ONE
|
|
19
|
+
* `decide()` gates them (no handler-level root check): list/get need `read`, create needs `create`,
|
|
20
|
+
* edit needs `update`, and the three key routes (issue/rotate/revoke) need `manage_keys` — the more-
|
|
21
|
+
* sensitive credential capability, grantable separately from editing the account.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ The actor + tenant are ALWAYS `req.principal` (the enforcement guard attached it) — the account
|
|
24
|
+
* is created in the ADMIN's own tenant, never a body-supplied one. ⛔ Own-tenant wall + ADR-0029
|
|
25
|
+
* ceiling live in the service; a cross-tenant id is 404 (no existence leak), an over-reach is 403.
|
|
26
|
+
* ⛔ Responses carry LABELS + names (never a raw secret; the id is a route handle only). The raw
|
|
27
|
+
* `ask_…` key is returned EXACTLY ONCE by issue/rotate.
|
|
28
|
+
*/
|
|
29
|
+
const common_1 = require("@nestjs/common");
|
|
30
|
+
const guard_1 = require("@astram/guard");
|
|
31
|
+
const service_account_service_js_1 = require("./service-account.service.js");
|
|
32
|
+
/** Project the resolved Principal to the AdminPrincipal the service consumes. */
|
|
33
|
+
function adminOf(req) {
|
|
34
|
+
const p = req.principal;
|
|
35
|
+
return {
|
|
36
|
+
id: p.id,
|
|
37
|
+
tenantId: p.tenantId,
|
|
38
|
+
scopeSubtreeNodeIds: p.scopeSubtreeNodeIds ?? [],
|
|
39
|
+
// No roleIds: the principal's are a login-time credential snapshot. The service resolves the
|
|
40
|
+
// admin's in-effect roles itself (HeldRoleResolver → readRoleIdsForTenant).
|
|
41
|
+
isRoot: p.isRoot === true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Map a service error to the right HTTP status (404 / 403 / 400); rethrow the rest (→ 503). */
|
|
45
|
+
function mapError(e) {
|
|
46
|
+
if (e instanceof service_account_service_js_1.ServiceAccountNotFoundError)
|
|
47
|
+
throw new common_1.NotFoundException(e.message);
|
|
48
|
+
if (e instanceof service_account_service_js_1.ServiceAccountCeilingError)
|
|
49
|
+
throw new common_1.ForbiddenException(e.message);
|
|
50
|
+
if (e instanceof service_account_service_js_1.ServiceAccountInputError)
|
|
51
|
+
throw new common_1.BadRequestException(e.message);
|
|
52
|
+
if (e instanceof service_account_service_js_1.ServiceAccountConflictError)
|
|
53
|
+
throw new common_1.ConflictException(e.message);
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
/** Narrow the OPTIONAL string[] fields of a body (scope/roles) — reject a non-string-array. */
|
|
57
|
+
function optStringArray(v, field) {
|
|
58
|
+
if (v === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string')) {
|
|
61
|
+
throw new common_1.BadRequestException(`${field} must be an array of strings`);
|
|
62
|
+
}
|
|
63
|
+
return v;
|
|
64
|
+
}
|
|
65
|
+
/** Narrow an OPTIONAL positive-integer expiry (epoch-ms) from a key-issue body. */
|
|
66
|
+
function optExpiry(v) {
|
|
67
|
+
if (v === undefined || v === null)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {
|
|
70
|
+
throw new common_1.BadRequestException('expiresAtMs must be a positive epoch-ms number');
|
|
71
|
+
}
|
|
72
|
+
return v;
|
|
73
|
+
}
|
|
74
|
+
let ServiceAccountController = class ServiceAccountController {
|
|
75
|
+
svc;
|
|
76
|
+
constructor(svc) {
|
|
77
|
+
this.svc = svc;
|
|
78
|
+
}
|
|
79
|
+
/** List the caller's own-tenant service accounts (by label). Gated `service_account:read`. */
|
|
80
|
+
async list(req) {
|
|
81
|
+
try {
|
|
82
|
+
return { accounts: await this.svc.list(adminOf(req)) };
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
mapError(e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* What the caller may grant an agent — the create/edit picker source (Phase 4). Gated
|
|
90
|
+
* `service_account:create` (only someone who can create agents needs the list). Computed from the
|
|
91
|
+
* SAME ceiling the create/update write check enforces: `{ roles: [{id,name}], scopeNodes:
|
|
92
|
+
* [{id,name,parentId|null}] }`. ⚠ Declared BEFORE `service-accounts/:id` so the static path wins.
|
|
93
|
+
*/
|
|
94
|
+
async assignable(req) {
|
|
95
|
+
try {
|
|
96
|
+
return await this.svc.assignable(adminOf(req));
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
mapError(e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Fetch one own-tenant account. Gated `service_account:read`. Cross-tenant/unknown → 404. */
|
|
103
|
+
async get(req, id) {
|
|
104
|
+
try {
|
|
105
|
+
return { account: await this.svc.get(adminOf(req), id) };
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
mapError(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create an account in the caller's own tenant. Gated `service_account:create`.
|
|
113
|
+
* Body: `{ label, scopeSubtreeNodeIds?, roleIds? }`. Scope/roles must pass the ADR-0029 ceiling.
|
|
114
|
+
*/
|
|
115
|
+
async create(req, body) {
|
|
116
|
+
const b = (body ?? {});
|
|
117
|
+
if (typeof b.label !== 'string')
|
|
118
|
+
throw new common_1.BadRequestException('label is required');
|
|
119
|
+
const scope = optStringArray(b.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds');
|
|
120
|
+
const roles = optStringArray(b.roleIds, 'roleIds');
|
|
121
|
+
try {
|
|
122
|
+
const account = await this.svc.create(adminOf(req), {
|
|
123
|
+
label: b.label,
|
|
124
|
+
...(scope !== undefined ? { scopeSubtreeNodeIds: scope } : {}),
|
|
125
|
+
...(roles !== undefined ? { roleIds: roles } : {}),
|
|
126
|
+
});
|
|
127
|
+
return { account };
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
mapError(e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Edit an account (label/scope/roles/status). Gated `service_account:update`. Own-tenant only;
|
|
135
|
+
* scope/roles re-checked against the ceiling (an edit cannot escalate).
|
|
136
|
+
*/
|
|
137
|
+
async update(req, id, body) {
|
|
138
|
+
const b = (body ?? {});
|
|
139
|
+
if (b.label !== undefined && typeof b.label !== 'string') {
|
|
140
|
+
throw new common_1.BadRequestException('label must be a string');
|
|
141
|
+
}
|
|
142
|
+
if (b.status !== undefined && b.status !== 'active' && b.status !== 'disabled') {
|
|
143
|
+
throw new common_1.BadRequestException("status must be 'active' or 'disabled'");
|
|
144
|
+
}
|
|
145
|
+
const scope = optStringArray(b.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds');
|
|
146
|
+
const roles = optStringArray(b.roleIds, 'roleIds');
|
|
147
|
+
try {
|
|
148
|
+
const account = await this.svc.update(adminOf(req), id, {
|
|
149
|
+
...(b.label !== undefined ? { label: b.label } : {}),
|
|
150
|
+
...(scope !== undefined ? { scopeSubtreeNodeIds: scope } : {}),
|
|
151
|
+
...(roles !== undefined ? { roleIds: roles } : {}),
|
|
152
|
+
...(b.status !== undefined ? { status: b.status } : {}),
|
|
153
|
+
});
|
|
154
|
+
return { account };
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
mapError(e);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Issue a NEW key for an account. Gated `service_account:manage_keys`. Body: `{ expiresAtMs? }`.
|
|
162
|
+
* Returns the raw `ask_…` key EXACTLY ONCE — copy it now, it is never retrievable again.
|
|
163
|
+
*/
|
|
164
|
+
async issueKey(req, id, body) {
|
|
165
|
+
const b = (body ?? {});
|
|
166
|
+
const expiresAtMs = optExpiry(b.expiresAtMs);
|
|
167
|
+
try {
|
|
168
|
+
const key = await this.svc.issueKey(adminOf(req), id, expiresAtMs !== undefined ? { expiresAtMs } : {});
|
|
169
|
+
return { key };
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
mapError(e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Rotate an account's key. Gated `service_account:manage_keys`. The previous key stops
|
|
177
|
+
* authenticating; the new raw key is returned EXACTLY ONCE.
|
|
178
|
+
*/
|
|
179
|
+
async rotateKey(req, id, body) {
|
|
180
|
+
const b = (body ?? {});
|
|
181
|
+
const expiresAtMs = optExpiry(b.expiresAtMs);
|
|
182
|
+
try {
|
|
183
|
+
const key = await this.svc.rotateKey(adminOf(req), id, expiresAtMs !== undefined ? { expiresAtMs } : {});
|
|
184
|
+
return { key };
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
mapError(e);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Revoke ALL of an account's keys. Gated `service_account:manage_keys`. Fail-closed on the next
|
|
192
|
+
* request; idempotent.
|
|
193
|
+
*/
|
|
194
|
+
async revokeKeys(req, id) {
|
|
195
|
+
try {
|
|
196
|
+
await this.svc.revokeKeys(adminOf(req), id);
|
|
197
|
+
return { success: true };
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
mapError(e);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
exports.ServiceAccountController = ServiceAccountController;
|
|
205
|
+
__decorate([
|
|
206
|
+
(0, common_1.Get)('service-accounts'),
|
|
207
|
+
(0, guard_1.Classified)({ typeKey: 'service_account', action: 'read' }),
|
|
208
|
+
__param(0, (0, common_1.Req)()),
|
|
209
|
+
__metadata("design:type", Function),
|
|
210
|
+
__metadata("design:paramtypes", [Object]),
|
|
211
|
+
__metadata("design:returntype", Promise)
|
|
212
|
+
], ServiceAccountController.prototype, "list", null);
|
|
213
|
+
__decorate([
|
|
214
|
+
(0, common_1.Get)('service-accounts/assignable'),
|
|
215
|
+
(0, guard_1.Classified)({ typeKey: 'service_account', action: 'create' }),
|
|
216
|
+
__param(0, (0, common_1.Req)()),
|
|
217
|
+
__metadata("design:type", Function),
|
|
218
|
+
__metadata("design:paramtypes", [Object]),
|
|
219
|
+
__metadata("design:returntype", Promise)
|
|
220
|
+
], ServiceAccountController.prototype, "assignable", null);
|
|
221
|
+
__decorate([
|
|
222
|
+
(0, common_1.Get)('service-accounts/:id'),
|
|
223
|
+
(0, guard_1.Classified)({ typeKey: 'service_account', action: 'read' }),
|
|
224
|
+
__param(0, (0, common_1.Req)()),
|
|
225
|
+
__param(1, (0, common_1.Param)('id')),
|
|
226
|
+
__metadata("design:type", Function),
|
|
227
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
228
|
+
__metadata("design:returntype", Promise)
|
|
229
|
+
], ServiceAccountController.prototype, "get", null);
|
|
230
|
+
__decorate([
|
|
231
|
+
(0, common_1.Post)('service-accounts'),
|
|
232
|
+
(0, guard_1.Classified)({ typeKey: 'service_account', action: 'create' }),
|
|
233
|
+
__param(0, (0, common_1.Req)()),
|
|
234
|
+
__param(1, (0, common_1.Body)()),
|
|
235
|
+
__metadata("design:type", Function),
|
|
236
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
237
|
+
__metadata("design:returntype", Promise)
|
|
238
|
+
], ServiceAccountController.prototype, "create", null);
|
|
239
|
+
__decorate([
|
|
240
|
+
(0, common_1.Patch)('service-accounts/:id'),
|
|
241
|
+
(0, guard_1.Classified)({ typeKey: 'service_account', action: 'update' }),
|
|
242
|
+
__param(0, (0, common_1.Req)()),
|
|
243
|
+
__param(1, (0, common_1.Param)('id')),
|
|
244
|
+
__param(2, (0, common_1.Body)()),
|
|
245
|
+
__metadata("design:type", Function),
|
|
246
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
247
|
+
__metadata("design:returntype", Promise)
|
|
248
|
+
], ServiceAccountController.prototype, "update", null);
|
|
249
|
+
__decorate([
|
|
250
|
+
(0, common_1.Post)('service-accounts/:id/keys'),
|
|
251
|
+
(0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
|
|
252
|
+
__param(0, (0, common_1.Req)()),
|
|
253
|
+
__param(1, (0, common_1.Param)('id')),
|
|
254
|
+
__param(2, (0, common_1.Body)()),
|
|
255
|
+
__metadata("design:type", Function),
|
|
256
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
257
|
+
__metadata("design:returntype", Promise)
|
|
258
|
+
], ServiceAccountController.prototype, "issueKey", null);
|
|
259
|
+
__decorate([
|
|
260
|
+
(0, common_1.Post)('service-accounts/:id/keys/rotate'),
|
|
261
|
+
(0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
|
|
262
|
+
__param(0, (0, common_1.Req)()),
|
|
263
|
+
__param(1, (0, common_1.Param)('id')),
|
|
264
|
+
__param(2, (0, common_1.Body)()),
|
|
265
|
+
__metadata("design:type", Function),
|
|
266
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
267
|
+
__metadata("design:returntype", Promise)
|
|
268
|
+
], ServiceAccountController.prototype, "rotateKey", null);
|
|
269
|
+
__decorate([
|
|
270
|
+
(0, common_1.Post)('service-accounts/:id/keys/revoke'),
|
|
271
|
+
(0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
|
|
272
|
+
__param(0, (0, common_1.Req)()),
|
|
273
|
+
__param(1, (0, common_1.Param)('id')),
|
|
274
|
+
__metadata("design:type", Function),
|
|
275
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
276
|
+
__metadata("design:returntype", Promise)
|
|
277
|
+
], ServiceAccountController.prototype, "revokeKeys", null);
|
|
278
|
+
exports.ServiceAccountController = ServiceAccountController = __decorate([
|
|
279
|
+
(0, common_1.Controller)('identity'),
|
|
280
|
+
__metadata("design:paramtypes", [service_account_service_js_1.ServiceAccountService])
|
|
281
|
+
], ServiceAccountController);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { PasswordHasher, ApiKeyStore } from '@astram/core';
|
|
2
|
+
import type { ServiceAccountStore, RoleOption, ScopeNodeOption } from './service-account.store.js';
|
|
3
|
+
/** The account id was not in the caller's subtree (unknown or cross-tenant) → 404 (no leak). */
|
|
4
|
+
export declare class ServiceAccountNotFoundError extends Error {
|
|
5
|
+
constructor();
|
|
6
|
+
}
|
|
7
|
+
/** The requested scope/roles exceed the caller's own authority (ADR-0029) → 403. */
|
|
8
|
+
export declare class ServiceAccountCeilingError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/** The account changed between this edit's read and its write (a concurrent edit) → 409. */
|
|
12
|
+
export declare class ServiceAccountConflictError extends Error {
|
|
13
|
+
constructor();
|
|
14
|
+
}
|
|
15
|
+
/** A malformed request (empty label, no fields to update, etc.) → 400. */
|
|
16
|
+
export declare class ServiceAccountInputError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
/** An account view for the API (never carries a raw secret; `id` is a handle, shown by label). */
|
|
20
|
+
export interface ServiceAccountView {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly scopeSubtreeNodeIds: readonly string[];
|
|
24
|
+
/** Resolved company names, same order as `scopeSubtreeNodeIds` (ADR-0012 — show these). */
|
|
25
|
+
readonly scopeLabels: readonly string[];
|
|
26
|
+
readonly roleIds: readonly string[];
|
|
27
|
+
/** Resolved role names, same order as `roleIds` (ADR-0012 — show these). */
|
|
28
|
+
readonly roleLabels: readonly string[];
|
|
29
|
+
readonly status: string;
|
|
30
|
+
readonly createdByName: string | null;
|
|
31
|
+
readonly createdAtMs: number;
|
|
32
|
+
readonly updatedAtMs: number | null;
|
|
33
|
+
}
|
|
34
|
+
/** The ONE-TIME key reveal returned by issue/rotate — the only time the raw key exists. */
|
|
35
|
+
export interface IssuedKey {
|
|
36
|
+
readonly apiKey: string;
|
|
37
|
+
readonly accountLabel: string;
|
|
38
|
+
readonly version: number;
|
|
39
|
+
readonly expiresAtMs: number | null;
|
|
40
|
+
}
|
|
41
|
+
/** A minimal audit sink (best-effort). The module adapts it to the shield_audit sink. */
|
|
42
|
+
export interface ServiceAccountAudit {
|
|
43
|
+
record(event: {
|
|
44
|
+
action: string;
|
|
45
|
+
actorId: string;
|
|
46
|
+
tenantId: string;
|
|
47
|
+
occurredAtMs: number;
|
|
48
|
+
attributes: Readonly<Record<string, unknown>>;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/** Resolve a principal id → display name (no raw uuid in the view). Returns null when unknown. */
|
|
52
|
+
export type NameResolver = (id: string) => Promise<string | null>;
|
|
53
|
+
/** The caller (an admin) as the service needs it — the enforcement guard's resolved principal. */
|
|
54
|
+
export interface AdminPrincipal {
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly tenantId: string;
|
|
57
|
+
readonly scopeSubtreeNodeIds: readonly string[];
|
|
58
|
+
readonly isRoot: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The roles an admin is IN EFFECT holding in a tenant — the ceiling's role source. Wired to
|
|
62
|
+
* `readRoleIdsForTenant` (the single source shared with gate 5 and `GrantCeiling`). Deliberately
|
|
63
|
+
* NOT the request principal's `roleIds`: that is the credential's login-time snapshot, which goes
|
|
64
|
+
* stale when postings change (a demoted admin could otherwise mint an agent with the role they lost).
|
|
65
|
+
*/
|
|
66
|
+
export type HeldRoleResolver = (principalId: string, tenantId: string) => Promise<readonly string[]>;
|
|
67
|
+
/**
|
|
68
|
+
* "What may I grant" — the ADR-0029 ceiling of ONE admin, computed in ONE place. Both the write
|
|
69
|
+
* check ({@link ServiceAccountService} create/update) and the picker source
|
|
70
|
+
* ({@link ServiceAccountService.assignable}) read it, so the picker can never offer what the write
|
|
71
|
+
* would reject. Root (strict `isRoot === true`) spans everything; a non-root admin may grant only
|
|
72
|
+
* nodes in their reachable subtree (their tenant when the subtree is empty) and roles they hold.
|
|
73
|
+
*/
|
|
74
|
+
export interface ServiceAccountCeiling {
|
|
75
|
+
readonly isRoot: boolean;
|
|
76
|
+
readonly nodeIds: ReadonlySet<string>;
|
|
77
|
+
readonly roleIds: ReadonlySet<string>;
|
|
78
|
+
}
|
|
79
|
+
export declare function ceilingOf(admin: AdminPrincipal, heldRoleIds: readonly string[]): ServiceAccountCeiling;
|
|
80
|
+
export declare function ceilingAllowsNode(c: ServiceAccountCeiling, nodeId: string): boolean;
|
|
81
|
+
export declare function ceilingAllowsRole(c: ServiceAccountCeiling, roleId: string): boolean;
|
|
82
|
+
/** What the caller may grant an agent (the picker source) — names, never shown as ids. */
|
|
83
|
+
export interface AssignableAccess {
|
|
84
|
+
readonly roles: readonly RoleOption[];
|
|
85
|
+
readonly scopeNodes: readonly ScopeNodeOption[];
|
|
86
|
+
}
|
|
87
|
+
export declare class ServiceAccountService {
|
|
88
|
+
private readonly accounts;
|
|
89
|
+
private readonly keys;
|
|
90
|
+
private readonly hasher;
|
|
91
|
+
private readonly audit;
|
|
92
|
+
private readonly resolveName;
|
|
93
|
+
private readonly heldRoles;
|
|
94
|
+
private readonly now;
|
|
95
|
+
constructor(accounts: ServiceAccountStore, keys: ApiKeyStore & {
|
|
96
|
+
putVersion(input: {
|
|
97
|
+
accountId: string;
|
|
98
|
+
tenantId: string;
|
|
99
|
+
scopeSubtreeNodeIds: readonly string[];
|
|
100
|
+
roleIds: readonly string[];
|
|
101
|
+
secretHash: string;
|
|
102
|
+
version: number;
|
|
103
|
+
expiresAtMs?: number;
|
|
104
|
+
createdAtMs: number;
|
|
105
|
+
}): Promise<void>;
|
|
106
|
+
currentVersion(accountId: string): Promise<number>;
|
|
107
|
+
revoke(accountId: string, atMs: number): Promise<void>;
|
|
108
|
+
}, hasher: PasswordHasher, audit: ServiceAccountAudit, resolveName: NameResolver, heldRoles: HeldRoleResolver, now?: () => number);
|
|
109
|
+
/**
|
|
110
|
+
* Rows → views, resolving role + company NAMES in two batched reads (no N+1). Labels follow the
|
|
111
|
+
* id order; an id that no longer resolves gets a fallback label, never the id (ADR-0012).
|
|
112
|
+
*/
|
|
113
|
+
private toViews;
|
|
114
|
+
/**
|
|
115
|
+
* The caller's ceiling with roles resolved LIVE ({@link HeldRoleResolver}). Root needs no role
|
|
116
|
+
* read. A resolver failure propagates (→ 503): fail closed, never an empty-or-stale ceiling.
|
|
117
|
+
*/
|
|
118
|
+
private ceilingFor;
|
|
119
|
+
/**
|
|
120
|
+
* Enforce the ADR-0029 ceiling ({@link ceilingOf}): every requested node must be within the
|
|
121
|
+
* ceiling's subtree and every requested role one the admin holds. Root bypasses. On violation
|
|
122
|
+
* throws a 403 — never with an id (ADR-0012), and NAMING an offender only when it is already on
|
|
123
|
+
* the agent being edited (`onAgent`, whose labels the caller sees anyway). Any other offender is
|
|
124
|
+
* described generically: the role/company catalog reads are not caller-scoped, so resolving a
|
|
125
|
+
* name for an arbitrary id would turn the 403 into a cross-tenant name/existence oracle.
|
|
126
|
+
*/
|
|
127
|
+
private assertWithinCeiling;
|
|
128
|
+
/**
|
|
129
|
+
* "the role ‘A’", "‘A’ and 2 other roles", "a role" … — names only offenders already on the agent.
|
|
130
|
+
*/
|
|
131
|
+
private describeOffenders;
|
|
132
|
+
/**
|
|
133
|
+
* Resolve names for audit (ids already authorised for this caller: within ceiling, or on the
|
|
134
|
+
* agent). Best-effort like the audit itself: it runs AFTER the write, so a lookup failure falls
|
|
135
|
+
* back to placeholder names instead of failing a write that already happened.
|
|
136
|
+
*/
|
|
137
|
+
private auditNames;
|
|
138
|
+
/**
|
|
139
|
+
* What the caller may grant (the picker source for create/edit), computed from the SAME
|
|
140
|
+
* {@link ceilingOf} the write check enforces. Root → every role + every company; non-root → the
|
|
141
|
+
* roles they hold + the nodes of their reachable subtree. Nodes come back tree-ordered with a
|
|
142
|
+
* `parentId` only when the parent is also assignable.
|
|
143
|
+
*/
|
|
144
|
+
assignable(admin: AdminPrincipal): Promise<AssignableAccess>;
|
|
145
|
+
/** List the admin's own-tenant accounts (by label; ids are handles). */
|
|
146
|
+
list(admin: AdminPrincipal): Promise<ServiceAccountView[]>;
|
|
147
|
+
/** Fetch one own-tenant account; cross-tenant/unknown → 404. */
|
|
148
|
+
get(admin: AdminPrincipal, id: string): Promise<ServiceAccountView>;
|
|
149
|
+
/**
|
|
150
|
+
* Create an account. `tenantId` is the admin's (server-stamped) — an account is created in the
|
|
151
|
+
* admin's OWN tenant only (a non-root admin cannot pick another tenant). Scope defaults to the
|
|
152
|
+
* admin's whole subtree when omitted; whatever is requested must pass the ceiling.
|
|
153
|
+
*/
|
|
154
|
+
create(admin: AdminPrincipal, input: {
|
|
155
|
+
label: string;
|
|
156
|
+
scopeSubtreeNodeIds?: readonly string[];
|
|
157
|
+
roleIds?: readonly string[];
|
|
158
|
+
}): Promise<ServiceAccountView>;
|
|
159
|
+
/**
|
|
160
|
+
* Edit an account's label/scope/roles/status (own-tenant; ceiling re-checked).
|
|
161
|
+
*
|
|
162
|
+
* Scope/roles semantics (Phase 4, confirmed 2026-10-08):
|
|
163
|
+
* - `scopeSubtreeNodeIds: []` → 400 (an agent needs at least one scope node).
|
|
164
|
+
* - What is REQUESTED must be ⊆ the editor's ceiling, else 403 — whatever the request shape
|
|
165
|
+
* (duplicates, mixed lists, or re-sending an item the agent already has but the editor can't
|
|
166
|
+
* grant). Nothing is written on a 403.
|
|
167
|
+
* - NON-ROOT: the agent's EXISTING items outside the editor's ceiling are PRESERVED —
|
|
168
|
+
* stored = requested ∪ (existing \ ceiling). A lower-ceiling admin can't strip access they could
|
|
169
|
+
* never grant back; the preserved items come from the stored row, never the request, so the
|
|
170
|
+
* merge can't add anything new. ROOT replaces with exactly what it sends.
|
|
171
|
+
*/
|
|
172
|
+
update(admin: AdminPrincipal, id: string, patch: {
|
|
173
|
+
label?: string;
|
|
174
|
+
scopeSubtreeNodeIds?: readonly string[];
|
|
175
|
+
roleIds?: readonly string[];
|
|
176
|
+
status?: 'active' | 'disabled';
|
|
177
|
+
}): Promise<ServiceAccountView>;
|
|
178
|
+
/**
|
|
179
|
+
* Issue a NEW key for an account (own-tenant). Mints `ask_<id>_<secret>`, hashes the secret, and
|
|
180
|
+
* persists the hash as the next version — carrying the ACCOUNT's current scope/roles onto the key
|
|
181
|
+
* (so the key's Principal reflects the account as it stands). Returns the raw key ONCE.
|
|
182
|
+
* `expiresAtMs` is optional (a non-expiring key is discouraged; the caller opts in to no expiry).
|
|
183
|
+
*/
|
|
184
|
+
issueKey(admin: AdminPrincipal, id: string, opts?: {
|
|
185
|
+
expiresAtMs?: number;
|
|
186
|
+
}): Promise<IssuedKey>;
|
|
187
|
+
/**
|
|
188
|
+
* Rotate an account's key: mint a new version (the old version stops being the current one —
|
|
189
|
+
* `getByAccountId` returns the highest, so the previous key no longer authenticates). Returns the
|
|
190
|
+
* new raw key ONCE.
|
|
191
|
+
*/
|
|
192
|
+
rotateKey(admin: AdminPrincipal, id: string, opts?: {
|
|
193
|
+
expiresAtMs?: number;
|
|
194
|
+
}): Promise<IssuedKey>;
|
|
195
|
+
/** Revoke ALL of an account's keys (own-tenant) — fail-closed on the next request. Idempotent. */
|
|
196
|
+
revokeKeys(admin: AdminPrincipal, id: string): Promise<void>;
|
|
197
|
+
/** Shared mint path for issue + rotate: version bump, hash, putVersion, audit, one-time reveal. */
|
|
198
|
+
private mintFor;
|
|
199
|
+
/** Best-effort audit — a sink throw must never fail a legitimate operation. */
|
|
200
|
+
private safeAudit;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=service-account.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-account.service.d.ts","sourceRoot":"","sources":["../../../src/features/service-account/service-account.service.ts"],"names":[],"mappings":"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"}
|