@astram/identity 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-facts-keys.d.ts +16 -0
- package/dist/auth-facts-keys.d.ts.map +1 -1
- package/dist/auth-facts-keys.js +26 -10
- package/dist/cross-tenant-grant.object.d.ts +2 -2
- package/dist/cross-tenant-grant.object.d.ts.map +1 -1
- package/dist/cross-tenant-grant.object.js +5 -2
- package/dist/features/authz/catalog-bootstrap.provider.d.ts +4 -1
- package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -1
- package/dist/features/authz/catalog-bootstrap.provider.js +37 -6
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts +6 -5
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts.map +1 -1
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.js +6 -5
- package/dist/features/files/files.controller.d.ts +27 -0
- package/dist/features/files/files.controller.d.ts.map +1 -0
- package/dist/features/files/files.controller.js +94 -0
- package/dist/features/files/files.service.d.ts +87 -0
- package/dist/features/files/files.service.d.ts.map +1 -0
- package/dist/features/files/files.service.js +311 -0
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +37 -11
- package/dist/features/policy/policy.service.d.ts +2 -1
- package/dist/features/policy/policy.service.d.ts.map +1 -1
- package/dist/features/policy/policy.service.js +15 -0
- package/dist/features/share/share.service.d.ts +19 -2
- package/dist/features/share/share.service.d.ts.map +1 -1
- package/dist/features/share/share.service.js +36 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +2 -0
- package/dist/record-share.object.d.ts +1 -1
- package/dist/record-share.object.d.ts.map +1 -1
- package/dist/record-share.object.js +4 -1
- package/dist/service-account.object.d.ts +3 -2
- package/dist/service-account.object.d.ts.map +1 -1
- package/dist/service-account.object.js +4 -2
- package/dist/stored-file.object.d.ts +35 -0
- package/dist/stored-file.object.d.ts.map +1 -0
- package/dist/stored-file.object.js +94 -0
- package/migrations/1710000000022-CreateStoredFile.cjs +43 -0
- package/package.json +8 -4
|
@@ -0,0 +1,311 @@
|
|
|
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 FilesService_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.FilesService = exports.DEFAULT_SERVABLE_CONTENT_TYPES = void 0;
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
const common_1 = require("@nestjs/common");
|
|
16
|
+
const core_1 = require("@astram/core");
|
|
17
|
+
const engine_1 = require("@astram/engine");
|
|
18
|
+
const TYPE_KEY = 'stored_file';
|
|
19
|
+
/** Fixed messages: never a key, a name, bytes or a store detail (SPEC §11 secrets, §13). */
|
|
20
|
+
const MSG_NOT_CONFIGURED = 'File storage is not configured';
|
|
21
|
+
const MSG_UNAVAILABLE = 'File storage is unavailable';
|
|
22
|
+
const MSG_FORBIDDEN = 'Forbidden';
|
|
23
|
+
/** Served when a stored content type is not one the intake allowlist produces (KI #13). */
|
|
24
|
+
const OCTET_STREAM = 'application/octet-stream';
|
|
25
|
+
/**
|
|
26
|
+
* The canonical content types the intake allowlist can store: today the `xlsx` profile, the only
|
|
27
|
+
* profile `ShieldModule` registers on `SHIELD_FILE_INGEST`. Pass the app's own list when it
|
|
28
|
+
* registers more profiles.
|
|
29
|
+
*/
|
|
30
|
+
exports.DEFAULT_SERVABLE_CONTENT_TYPES = core_1.XLSX_PROFILE.contentTypes;
|
|
31
|
+
/**
|
|
32
|
+
* True when `key` is exactly `t/<tenantId>/<uuid>` for this tenant (KI #4). Both segments must be
|
|
33
|
+
* canonical uuids, so a traversal (`..`), an extra segment or a shared string prefix
|
|
34
|
+
* (`t/<tenant>extra/…`) never matches.
|
|
35
|
+
*/
|
|
36
|
+
function keyBelongsTo(key, tenantId) {
|
|
37
|
+
const tenant = (0, engine_1.canonicalUuid)(tenantId);
|
|
38
|
+
if (tenant === null || typeof key !== 'string')
|
|
39
|
+
return false;
|
|
40
|
+
const parts = key.split('/');
|
|
41
|
+
return (parts.length === 3 &&
|
|
42
|
+
parts[0] === 't' &&
|
|
43
|
+
parts[1] === tenant &&
|
|
44
|
+
(0, engine_1.canonicalUuid)(parts[2]) === parts[2]);
|
|
45
|
+
}
|
|
46
|
+
const sha256Hex = (bytes) => (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
|
|
47
|
+
/**
|
|
48
|
+
* FilesService — stored files on the engine pipe (file-storage SPEC §5 I-K).
|
|
49
|
+
*
|
|
50
|
+
* - The `stored_file` row is written ONLY here (KI #4): the key comes from `FileIntake` (built from
|
|
51
|
+
* the session tenant) and is re-checked against the actor's tenant before any put or create.
|
|
52
|
+
* Tenant and owner are stamped by the pipe from the session (Shield #7), never sent from here.
|
|
53
|
+
* - Every row access goes through `RecordsService` (record gate, tenant wall, read scope, audit);
|
|
54
|
+
* no DB pool, no bypass flag (Shield #3, #9). The bucket is read only in `open()`, after the gate.
|
|
55
|
+
* - Errors map at source: 501 when no store is bound, 503 (fixed message) for store failures and
|
|
56
|
+
* integrity faults, 403 (fixed message) for missing, foreign or denied rows alike.
|
|
57
|
+
* - No actor argument (SPEC §5 I-K): the key tenant and the `file_access` audit actor come from
|
|
58
|
+
* `RecordsService.requestPrincipal()`, the same principal the engine stamps and gates with, so
|
|
59
|
+
* key tenant, row tenant/owner and audit actor cannot diverge. No principal ⇒ fail closed.
|
|
60
|
+
* - The engine does not audit its own denials, so a 403 from open/remove writes a `file_access`
|
|
61
|
+
* failure event (`denied_or_missing`, no key, no name). Logs carry the storage key only.
|
|
62
|
+
*/
|
|
63
|
+
let FilesService = FilesService_1 = class FilesService {
|
|
64
|
+
records;
|
|
65
|
+
objectStore;
|
|
66
|
+
audit;
|
|
67
|
+
logger = new common_1.Logger(FilesService_1.name);
|
|
68
|
+
orphans = 0;
|
|
69
|
+
constructor(records, objectStore, audit, servableContentTypes = exports.DEFAULT_SERVABLE_CONTENT_TYPES) {
|
|
70
|
+
this.records = records;
|
|
71
|
+
this.objectStore = objectStore;
|
|
72
|
+
this.audit = audit;
|
|
73
|
+
this.servable = new Map(servableContentTypes.map((t) => [t.toLowerCase(), t]));
|
|
74
|
+
}
|
|
75
|
+
/** Lower-cased allowlisted type -> its canonical spelling (what `open()` may serve). */
|
|
76
|
+
servable;
|
|
77
|
+
/**
|
|
78
|
+
* KI #13: the download Content-Type is re-checked against the intake allowlist, never taken
|
|
79
|
+
* from the row as-is. Anything else (a row written outside FileIntake, a profile since
|
|
80
|
+
* removed) is served as `application/octet-stream`.
|
|
81
|
+
*/
|
|
82
|
+
servableType(stored) {
|
|
83
|
+
if (typeof stored !== 'string')
|
|
84
|
+
return OCTET_STREAM;
|
|
85
|
+
return this.servable.get(stored.toLowerCase()) ?? OCTET_STREAM;
|
|
86
|
+
}
|
|
87
|
+
/** Objects left behind by a failed compensating or post-commit delete since boot. */
|
|
88
|
+
get orphanedObjectCount() {
|
|
89
|
+
return this.orphans;
|
|
90
|
+
}
|
|
91
|
+
/** Put the bytes, then create the `stored_file` record; on create failure delete the object. */
|
|
92
|
+
async store(intake) {
|
|
93
|
+
const store = this.requireStore();
|
|
94
|
+
const actor = this.records.requestPrincipal();
|
|
95
|
+
if (!keyBelongsTo(intake.storageKey, actor.tenantId)) {
|
|
96
|
+
// A key outside the session tenant never reaches the bucket or the table (KI #4).
|
|
97
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
98
|
+
}
|
|
99
|
+
const key = intake.storageKey;
|
|
100
|
+
try {
|
|
101
|
+
await store.put(key, intake.bytes, {
|
|
102
|
+
contentType: intake.contentType,
|
|
103
|
+
sha256: intake.sha256,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (cause) {
|
|
107
|
+
this.logger.error(`file put failed (${errorName(cause)}): ${key}`);
|
|
108
|
+
throw new common_1.ServiceUnavailableException(MSG_UNAVAILABLE);
|
|
109
|
+
}
|
|
110
|
+
let uuid;
|
|
111
|
+
try {
|
|
112
|
+
const id = await this.records.create(TYPE_KEY, {
|
|
113
|
+
fields: {
|
|
114
|
+
storageKey: key,
|
|
115
|
+
sha256: intake.sha256,
|
|
116
|
+
contentType: intake.contentType,
|
|
117
|
+
sizeBytes: intake.sizeBytes,
|
|
118
|
+
originalName: intake.originalName,
|
|
119
|
+
createdAt: Date.now(),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
uuid = String(id);
|
|
123
|
+
}
|
|
124
|
+
catch (cause) {
|
|
125
|
+
await this.deleteObject(store, key, 'compensating');
|
|
126
|
+
throw cause;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
uuid,
|
|
130
|
+
sha256: intake.sha256,
|
|
131
|
+
sizeBytes: intake.sizeBytes,
|
|
132
|
+
contentType: intake.contentType,
|
|
133
|
+
originalName: intake.originalName,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Read a file under record-level decide, with the key and sha256 re-checked (SPEC §5 J). */
|
|
137
|
+
async open(uuid) {
|
|
138
|
+
const store = this.requireStore();
|
|
139
|
+
const actor = this.records.requestPrincipal();
|
|
140
|
+
const id = (0, engine_1.canonicalUuid)(uuid);
|
|
141
|
+
let row;
|
|
142
|
+
try {
|
|
143
|
+
if (id === null)
|
|
144
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
145
|
+
row = await this.records.get(TYPE_KEY, id);
|
|
146
|
+
if (row === null)
|
|
147
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
148
|
+
}
|
|
149
|
+
catch (cause) {
|
|
150
|
+
if (isDenial(cause))
|
|
151
|
+
await this.auditDenied(actor, 'read', id);
|
|
152
|
+
throw cause;
|
|
153
|
+
}
|
|
154
|
+
const f = row.fields;
|
|
155
|
+
const recordTenant = f.tenantId;
|
|
156
|
+
if (!keyBelongsTo(f.storageKey, recordTenant)) {
|
|
157
|
+
// The row points outside its own tenant: an integrity fault, never a cross-tenant read.
|
|
158
|
+
this.logger.error(`stored_file ${String(row.id)} key outside its tenant prefix; refused`);
|
|
159
|
+
await this.auditFailure(row, actor, 'key_prefix_mismatch');
|
|
160
|
+
throw new common_1.ServiceUnavailableException(MSG_UNAVAILABLE);
|
|
161
|
+
}
|
|
162
|
+
const key = f.storageKey;
|
|
163
|
+
let bytes;
|
|
164
|
+
try {
|
|
165
|
+
({ bytes } = await store.get(key));
|
|
166
|
+
}
|
|
167
|
+
catch (cause) {
|
|
168
|
+
if (cause instanceof core_1.ObjectNotFoundError) {
|
|
169
|
+
this.logger.error(`stored object missing: ${key}`);
|
|
170
|
+
await this.auditFailure(row, actor, 'object_missing');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.logger.error(`file get failed (${errorName(cause)}): ${key}`);
|
|
174
|
+
}
|
|
175
|
+
throw new common_1.ServiceUnavailableException(MSG_UNAVAILABLE);
|
|
176
|
+
}
|
|
177
|
+
const sha = typeof f.sha256 === 'string' ? f.sha256.toLowerCase() : '';
|
|
178
|
+
if (sha.length !== 64 || sha256Hex(bytes) !== sha) {
|
|
179
|
+
this.logger.error(`stored object failed its sha256 check: ${key}`);
|
|
180
|
+
await this.auditFailure(row, actor, 'sha256_mismatch');
|
|
181
|
+
throw new common_1.ServiceUnavailableException(MSG_UNAVAILABLE);
|
|
182
|
+
}
|
|
183
|
+
const sizeBytes = Number(f.sizeBytes);
|
|
184
|
+
// Fail closed: no bytes leave without their read event (a sink error propagates).
|
|
185
|
+
await this.audit.record(this.event(actor, 'read', row, { sha256: sha, sizeBytes, outcome: 'success' }));
|
|
186
|
+
return {
|
|
187
|
+
bytes,
|
|
188
|
+
contentType: this.servableType(f.contentType),
|
|
189
|
+
originalName: String(f.originalName),
|
|
190
|
+
sizeBytes,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Hard delete (team default): the record in a transaction (gated and audited by the pipe), the
|
|
195
|
+
* object after commit, then one `file_access` delete event (SPEC §5 K). A foreign key is never
|
|
196
|
+
* passed to the store.
|
|
197
|
+
*/
|
|
198
|
+
async remove(uuid) {
|
|
199
|
+
const store = this.requireStore();
|
|
200
|
+
const actor = this.records.requestPrincipal();
|
|
201
|
+
const id = (0, engine_1.canonicalUuid)(uuid);
|
|
202
|
+
let row;
|
|
203
|
+
let key;
|
|
204
|
+
try {
|
|
205
|
+
if (id === null)
|
|
206
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
207
|
+
({ row, key } = await this.records.transaction(async (tx) => {
|
|
208
|
+
const found = await tx.get(TYPE_KEY, id);
|
|
209
|
+
if (found === null)
|
|
210
|
+
throw new common_1.ForbiddenException(MSG_FORBIDDEN);
|
|
211
|
+
await tx.delete(TYPE_KEY, id);
|
|
212
|
+
const k = found.fields.storageKey;
|
|
213
|
+
return { row: found, key: keyBelongsTo(k, found.fields.tenantId) ? k : null };
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
catch (cause) {
|
|
217
|
+
if (isDenial(cause))
|
|
218
|
+
await this.auditDenied(actor, 'delete', id);
|
|
219
|
+
throw cause;
|
|
220
|
+
}
|
|
221
|
+
let objectDeleted = false;
|
|
222
|
+
if (key === null) {
|
|
223
|
+
this.logger.error(`stored_file ${String(row.id)} key outside its tenant prefix; object not deleted`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
objectDeleted = await this.deleteObject(store, key, 'post-commit');
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
await this.audit.record(this.event(actor, 'delete', row, { objectDeleted, outcome: 'success' }));
|
|
230
|
+
}
|
|
231
|
+
catch (cause) {
|
|
232
|
+
// The record delete is committed and audited by the pipe (`admin_action`); a failed
|
|
233
|
+
// file_access row must not invite a retry that now answers 403.
|
|
234
|
+
this.logger.error(`file_access delete audit failed (${errorName(cause)})`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
requireStore() {
|
|
238
|
+
if (this.objectStore === null || this.objectStore === undefined) {
|
|
239
|
+
throw new common_1.NotImplementedException(MSG_NOT_CONFIGURED);
|
|
240
|
+
}
|
|
241
|
+
return this.objectStore;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Delete an object; true when the store confirmed it. On failure log the key only, count the
|
|
245
|
+
* orphan and answer false. Never throws.
|
|
246
|
+
*/
|
|
247
|
+
async deleteObject(store, key, phase) {
|
|
248
|
+
try {
|
|
249
|
+
await store.delete(key);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch (cause) {
|
|
253
|
+
this.orphans += 1;
|
|
254
|
+
this.logger.error(`orphaned object after ${phase} delete failed (${errorName(cause)}): ${key}`);
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A `file_access` failure event for a 403 (missing, foreign, malformed or engine-denied: one
|
|
260
|
+
* reason, since the caller may not learn which). Best effort; the 403 is thrown regardless.
|
|
261
|
+
*/
|
|
262
|
+
async auditDenied(actor, action, id) {
|
|
263
|
+
try {
|
|
264
|
+
await this.audit.record(this.event(actor, action, id, { outcome: 'failure', reason: 'denied_or_missing' }));
|
|
265
|
+
}
|
|
266
|
+
catch (cause) {
|
|
267
|
+
this.logger.error(`file_access denial audit failed (${errorName(cause)})`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** A `file_access` failure event (best effort; the 503 is thrown regardless). */
|
|
271
|
+
async auditFailure(row, actor, reason) {
|
|
272
|
+
try {
|
|
273
|
+
await this.audit.record(this.event(actor, 'read', row, { outcome: 'failure', reason }));
|
|
274
|
+
}
|
|
275
|
+
catch (cause) {
|
|
276
|
+
this.logger.error(`file_access failure audit failed (${errorName(cause)})`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** `target` is the fetched row, or for a denial only the requested uuid (null if malformed). */
|
|
280
|
+
event(actor, action, target, attributes) {
|
|
281
|
+
const row = typeof target === 'string' || target === null ? null : target;
|
|
282
|
+
const recordId = row !== null ? String(row.id) : target;
|
|
283
|
+
const recordTenant = row !== null ? (0, engine_1.canonicalUuid)(row.fields.tenantId) : null;
|
|
284
|
+
return {
|
|
285
|
+
type: 'file_access',
|
|
286
|
+
occurredAtMs: Date.now(),
|
|
287
|
+
actorId: actor.id,
|
|
288
|
+
tenantId: actor.tenantId,
|
|
289
|
+
action: { key: action },
|
|
290
|
+
resource: {
|
|
291
|
+
level: 'record',
|
|
292
|
+
typeKey: TYPE_KEY,
|
|
293
|
+
...(recordId !== null ? { recordId } : {}),
|
|
294
|
+
...(recordTenant !== null ? { tenantId: recordTenant } : {}),
|
|
295
|
+
},
|
|
296
|
+
attributes,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
exports.FilesService = FilesService;
|
|
301
|
+
exports.FilesService = FilesService = FilesService_1 = __decorate([
|
|
302
|
+
(0, common_1.Injectable)(),
|
|
303
|
+
__metadata("design:paramtypes", [Function, Object, Object, Array])
|
|
304
|
+
], FilesService);
|
|
305
|
+
/** A 403-class outcome: our own missing/foreign/malformed refusal or an engine gate deny. */
|
|
306
|
+
function isDenial(cause) {
|
|
307
|
+
return cause instanceof common_1.ForbiddenException || cause instanceof engine_1.AuthorizationDeniedError;
|
|
308
|
+
}
|
|
309
|
+
function errorName(cause) {
|
|
310
|
+
return cause instanceof Error ? cause.name : 'error';
|
|
311
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpE,OAAO,EAcL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAiGxB;;;;;;GAMG;AACH,wBAAgB,0CAA0C,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAUnF;AAoBD;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,GAClB,GAAE;QACD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;KACtC,GAAG,aAAa;CAigBvB"}
|
|
@@ -56,6 +56,8 @@ const audit_controller_js_1 = require("./audit/audit.controller.js");
|
|
|
56
56
|
const cross_tenant_grant_read_service_js_1 = require("./cross-tenant/cross-tenant-grant-read.service.js");
|
|
57
57
|
const cross_tenant_grant_controller_js_1 = require("./cross-tenant/cross-tenant-grant.controller.js");
|
|
58
58
|
const grant_authorize_js_1 = require("./cross-tenant/grant-authorize.js");
|
|
59
|
+
const files_service_js_1 = require("./files/files.service.js");
|
|
60
|
+
const files_controller_js_1 = require("./files/files.controller.js");
|
|
59
61
|
/** DI token of the provider that links the guard's Redis version cache to the bumper (ADR-0035). */
|
|
60
62
|
const IDENTITY_VERSION_PUBLISHER_LINK = Symbol('IDENTITY_VERSION_PUBLISHER_LINK');
|
|
61
63
|
/** DI token of the boot check that warns when a user disable cannot revoke sessions. */
|
|
@@ -155,10 +157,6 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
155
157
|
// A role delete (Phase 8 #2): its holder tenants, read before the delete cascades postings.
|
|
156
158
|
tenantsForRole: (roleUuid) => bumper.tenantsForRole(roleUuid, authzPool),
|
|
157
159
|
};
|
|
158
|
-
// The core ModuleRegistry the catalog reconcile job needs, built from the identity
|
|
159
|
-
// object registrations only (guard's bridge reflects decorated classes → registry): it
|
|
160
|
-
// catalogs the identity permissions. Authorities use the shared `recordAuthorization.registry`.
|
|
161
|
-
const identityRegistry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects]);
|
|
162
160
|
// The grant-admin collaborators built at forRoot() over the shared pool (mirrors the
|
|
163
161
|
// postingVersionBump closure pattern above). GrantCeiling (ADR-0029) enforces the
|
|
164
162
|
// self-escalation ceiling: a non-root grantor may only grant a key they themselves hold —
|
|
@@ -272,7 +270,15 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
272
270
|
// The read side (inbox/mine/incoming/offerable-types) over the same pool + the merged object
|
|
273
271
|
// set (offerable = tenantScoped types only, the consumer's included, KI #57) + the per-type
|
|
274
272
|
// action vocabulary.
|
|
275
|
-
|
|
273
|
+
// The ONE registration-based action lookup (ADR-0038): typeKey → the type's declared
|
|
274
|
+
// vocabulary (`effectiveActions`, validated) over identity + the consumer's extraObjects.
|
|
275
|
+
// `undefined` for an unregistered type. Shared by every action-vocabulary consumer here.
|
|
276
|
+
const actionsByTypeKey = new Map([...objects_js_1.identityObjects, ...extraObjects].map((input) => {
|
|
277
|
+
const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
|
|
278
|
+
return [reg.typeKey, (0, engine_1.effectiveActions)(reg)];
|
|
279
|
+
}));
|
|
280
|
+
const actionsByType = (typeKey) => actionsByTypeKey.get(typeKey);
|
|
281
|
+
const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects, ...extraObjects], (typeKey) => actionsByType(typeKey) ?? []);
|
|
276
282
|
// ── GUIDED RULE-BUILDER (list-scoping engine Phase 5, §9.1) ──────────────────────────────
|
|
277
283
|
// The policy-authoring surface over the guard `MariaDbPolicyStore` + core `makeAuthorPolicy`.
|
|
278
284
|
// Reuses the SAME shared registry + `crossTenantAuthority` (a DecisionAuthority over the
|
|
@@ -304,7 +310,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
304
310
|
return {
|
|
305
311
|
typeKey: reg.typeKey,
|
|
306
312
|
label: reg.typeKey,
|
|
307
|
-
actions: (
|
|
313
|
+
actions: actionsByType(reg.typeKey) ?? [],
|
|
308
314
|
fields: reg.fields.map((f) => ({ key: f.key, type: f.type })),
|
|
309
315
|
};
|
|
310
316
|
});
|
|
@@ -373,7 +379,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
373
379
|
},
|
|
374
380
|
},
|
|
375
381
|
});
|
|
376
|
-
const shareService = new share_service_js_1.ShareService(recordShareWriter, auditLabels);
|
|
382
|
+
const shareService = new share_service_js_1.ShareService(recordShareWriter, auditLabels, actionsByType);
|
|
377
383
|
// ── SERVICE ACCOUNTS + API KEYS (ADR-0033, Phase 2) ──────────────────────────────────────
|
|
378
384
|
// The machine-identity admin surface: create/scope service accounts + issue/rotate/revoke
|
|
379
385
|
// their `ask_…` API keys. Self-wired here over the SAME shared authzPool (which reaches BOTH
|
|
@@ -418,6 +424,13 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
418
424
|
// boundary made an identity-provided authenticator resolve to `undefined` → `ask_…` bearers
|
|
419
425
|
// 401'd). This module owns only the ACCOUNT ADMIN surface (store + service + controller); the
|
|
420
426
|
// guard owns authentication. (ADR-0033.)
|
|
427
|
+
// Stored files (file-storage SPEC §5 I-K): `file_access` events go to the SAME shield_audit
|
|
428
|
+
// sink over the shared authzPool. Not best-effort here: FilesService decides (a read event
|
|
429
|
+
// that cannot be written withholds the bytes).
|
|
430
|
+
const filesAuditSink = new engine_1.MariaDbAuditSink({}); // shield_audit.audit_log defaults
|
|
431
|
+
const filesAudit = {
|
|
432
|
+
record: (event) => filesAuditSink.record(event, (sql, params) => authzPool.query(sql, params)),
|
|
433
|
+
};
|
|
421
434
|
return {
|
|
422
435
|
module: IdentityFeatureModule_1,
|
|
423
436
|
imports: [
|
|
@@ -469,6 +482,9 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
469
482
|
// Service accounts (ADR-0033) — `service-accounts/*` collides with nothing; its static
|
|
470
483
|
// sub-paths (/keys, /keys/rotate, /keys/revoke) sit under `:id` and are method-distinct.
|
|
471
484
|
service_account_controller_js_1.ServiceAccountController,
|
|
485
|
+
// Stored-file download (file-storage SPEC §3) — `files/:uuid` collides with nothing.
|
|
486
|
+
// Gated on `stored_file:read`; answers 501 when no object store is bound. Appended last.
|
|
487
|
+
files_controller_js_1.FilesController,
|
|
472
488
|
],
|
|
473
489
|
providers: [
|
|
474
490
|
company_service_js_1.CompanyService,
|
|
@@ -541,10 +557,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
541
557
|
// enforcement guard can authenticate `ask_…` bearers out-of-the-box. Both useValue.
|
|
542
558
|
{ provide: service_account_service_js_1.ServiceAccountService, useValue: serviceAccountService },
|
|
543
559
|
// The mutating half of auto-catalog: a single CatalogReconcileJob (post-boot pass +
|
|
544
|
-
// cron), over the shared pool + the identity
|
|
560
|
+
// cron), over the shared pool + the ONE record-authorization registry (identity objects +
|
|
561
|
+
// `extraObjects`, KI #70): the catalog holds exactly the keys decide() and the grant
|
|
562
|
+
// ceiling know, so a consumer type's keys are grantable and its rows are not orphaned.
|
|
545
563
|
{
|
|
546
564
|
provide: catalog_reconcile_job_js_1.CatalogReconcileJob,
|
|
547
|
-
useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool,
|
|
565
|
+
useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, recordAuthorization.registry),
|
|
548
566
|
},
|
|
549
567
|
// The bundle half: reconciles the merged AccessBundleSpec[] (ENGINE_ALL_ACCESS_BUNDLES,
|
|
550
568
|
// reflected by the engine from identityAccessBundles) into shield_authz.access_bundle.
|
|
@@ -560,10 +578,18 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
560
578
|
// Boot: read-only verifyGatable (fail-closed). Post-boot: kicks the jobs once (catalog
|
|
561
579
|
// THEN bundles, THEN the scope-closure reconcile) + schedules them (DISABLE_ALL_CRONS-
|
|
562
580
|
// gated). Injects DiscoveryService/MetadataScanner/Reflector (from DiscoveryModule
|
|
563
|
-
// above), the jobs, the ScopeClosureService, and
|
|
581
|
+
// above), the jobs, the ScopeClosureService, ENGINE_ALL_REGISTRATIONS and the record
|
|
582
|
+
// authorization (whose registry is the verify key universe, KI #70).
|
|
564
583
|
catalog_bootstrap_provider_js_1.CatalogBootstrapProvider,
|
|
584
|
+
// Stored files (file-storage Phase 5): the request-principal RecordsService + the guard's
|
|
585
|
+
// object store (`null`/absent ⇒ every method answers 501) + the file_access audit bridge.
|
|
586
|
+
{
|
|
587
|
+
provide: files_service_js_1.FilesService,
|
|
588
|
+
useFactory: (records, store) => new files_service_js_1.FilesService(records, store ?? null, filesAudit),
|
|
589
|
+
inject: [engine_1.RecordsService, { token: guard_1.SHIELD_OBJECT_STORE, optional: true }],
|
|
590
|
+
},
|
|
565
591
|
],
|
|
566
|
-
exports: [company_service_js_1.CompanyService, role_service_js_1.RoleService, user_service_js_1.UserService],
|
|
592
|
+
exports: [company_service_js_1.CompanyService, role_service_js_1.RoleService, user_service_js_1.UserService, files_service_js_1.FilesService],
|
|
567
593
|
};
|
|
568
594
|
}
|
|
569
595
|
};
|
|
@@ -91,7 +91,8 @@ export interface PolicyServiceDeps {
|
|
|
91
91
|
/**
|
|
92
92
|
* The authorable-type field/action metadata (built in the module from the reflected object
|
|
93
93
|
* types). Drives `schema()` — the guided builder's field + operator dropdowns. A type absent
|
|
94
|
-
* here is NOT authorable via the builder (schema → NotFound)
|
|
94
|
+
* here is NOT authorable via the builder (schema → NotFound), and `author` rejects any non-`'*'`
|
|
95
|
+
* action for it (declared-action check, fail closed — SPEC v2 §5G).
|
|
95
96
|
*/
|
|
96
97
|
readonly schemaSource?: readonly TypeSchemaSource[];
|
|
97
98
|
readonly now?: () => number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.service.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAEV,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,MAAM,EACN,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,wFAAwF;AACxF,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,SAA2B;CAI/C;AAED,kGAAkG;AAClG,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,SAAoC;CAIxD;AAED,mGAAmG;AACnG,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,SAAsB;CAI1C;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC5E,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5E;AAED,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAE5F,0FAA0F;AAC1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,mGAAmG;IACnG,QAAQ,CAAC,cAAc,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACvD,6FAA6F;IAC7F,QAAQ,CAAC,cAAc,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACvD,wFAAwF;IACxF,QAAQ,CAAC,gBAAgB,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC1D;AAED,wFAAwF;AACxF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC/C;AAED,sEAAsE;AACtE,MAAM,MAAM,kBAAkB,GAC1B,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,QAAQ,GACR,IAAI,GACJ,IAAI,GACJ,KAAK,GACL,KAAK,GACL,UAAU,GACV,cAAc,GACd,SAAS,GACT,aAAa,CAAC;AAElB,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC9B;AAED,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;CACtC;AAUD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,kBAAkB,EAAE,CAgB7F;AAED,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,kBAAkB,GAAG,eAAe,CAAC;IAC3D,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B
|
|
1
|
+
{"version":3,"file":"policy.service.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAEV,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,MAAM,EACN,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,wFAAwF;AACxF,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,SAA2B;CAI/C;AAED,kGAAkG;AAClG,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,SAAoC;CAIxD;AAED,mGAAmG;AACnG,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,SAAsB;CAI1C;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC5E,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5E;AAED,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAE5F,0FAA0F;AAC1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,mGAAmG;IACnG,QAAQ,CAAC,cAAc,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACvD,6FAA6F;IAC7F,QAAQ,CAAC,cAAc,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACvD,wFAAwF;IACxF,QAAQ,CAAC,gBAAgB,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC1D;AAED,wFAAwF;AACxF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC/C;AAED,sEAAsE;AACtE,MAAM,MAAM,kBAAkB,GAC1B,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,QAAQ,GACR,IAAI,GACJ,IAAI,GACJ,KAAK,GACL,KAAK,GACL,UAAU,GACV,cAAc,GACd,SAAS,GACT,aAAa,CAAC;AAElB,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC9B;AAED,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;CACtC;AAUD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,kBAAkB,EAAE,CAgB7F;AAED,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,kBAAkB,GAAG,eAAe,CAAC;IAC3D,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsC;IACnE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;gBAEzD,IAAI,EAAE,iBAAiB;IAanC;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB;IAmBzC;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAqCrE,+FAA+F;IACzF,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAIzE;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAMhE"}
|
|
@@ -106,6 +106,21 @@ class PolicyService {
|
|
|
106
106
|
* reasons to typed errors; a transient throw PROPAGATES (→ 503).
|
|
107
107
|
*/
|
|
108
108
|
async author(actor, policy) {
|
|
109
|
+
// Declared-action check (SPEC v2 §5G): before core `makeAuthorPolicy`, so an undeclared
|
|
110
|
+
// action saves nothing and audits nothing. `'*'` stays allowed. Fail closed: with no schema
|
|
111
|
+
// source (or no entry for the type) every non-'*' action is undeclared.
|
|
112
|
+
// SPEC v3 §11 rule 6: a record rule on `create` is rejected even when `create` is declared.
|
|
113
|
+
// Record rules are evaluated per record (core gate 6) and the create gate is type level, so
|
|
114
|
+
// such a rule would never take effect. `'*'` stays allowed (it applies to the other actions).
|
|
115
|
+
if (policy.action === 'create') {
|
|
116
|
+
throw new InvalidPolicyError("Record rules are evaluated per record and never apply at create; a rule on action 'create' would never take effect. Use the object-type create grant instead.");
|
|
117
|
+
}
|
|
118
|
+
if (policy.action !== '*') {
|
|
119
|
+
const declared = this.schemaByType.get(policy.typeKey)?.actions;
|
|
120
|
+
if (declared === undefined || !declared.includes(policy.action)) {
|
|
121
|
+
throw new InvalidPolicyError(`Action '${policy.action}' is not declared on type '${policy.typeKey}'.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
109
124
|
const mutation = {
|
|
110
125
|
kind: 'author_policy',
|
|
111
126
|
admin: actor,
|
|
@@ -69,17 +69,34 @@ export interface ShareRequest {
|
|
|
69
69
|
readonly granteeUuid: string;
|
|
70
70
|
readonly action: string;
|
|
71
71
|
}
|
|
72
|
+
/** typeKey → the type's declared action vocabulary (`effectiveActions`); `undefined` = unknown type. */
|
|
73
|
+
export type ActionsByType = (typeKey: string) => readonly string[] | undefined;
|
|
72
74
|
export declare class ShareService {
|
|
73
75
|
private readonly writer;
|
|
74
76
|
private readonly labels;
|
|
75
|
-
|
|
77
|
+
private readonly actionsByType;
|
|
78
|
+
/**
|
|
79
|
+
* @param actionsByType the registration-based action lookup built in `forRoot` (SPEC v2 §5G).
|
|
80
|
+
* Required: share rejects an unknown type or an action the type does not declare (`'*'`
|
|
81
|
+
* stays allowed) with 400 BEFORE the writer is called; unshare rejects only an unknown type
|
|
82
|
+
* (SPEC v3 §5G: revoking cannot grant).
|
|
83
|
+
*/
|
|
84
|
+
constructor(writer: RecordShareWriter | ShareWriterLike, labels: ShareLabelResolver, actionsByType: ActionsByType);
|
|
85
|
+
/** 400 on an unknown type; returns the type's declared actions (§5G, §13). */
|
|
86
|
+
private assertKnownType;
|
|
87
|
+
/** 400 on an unknown type or an undeclared action — runs before any writer call (§5G). */
|
|
88
|
+
private assertDeclaredAction;
|
|
76
89
|
/**
|
|
77
90
|
* Share a record the caller owns with a user/role for an action. The tenant + sharer are the
|
|
78
91
|
* caller's own (from `actor`), NEVER the body — a forged owner/tenant cannot reach the writer.
|
|
79
92
|
* The writer proves `actor.id` owns the record before writing (fail-closed).
|
|
80
93
|
*/
|
|
81
94
|
share(actor: Principal, req: ShareRequest): Promise<void>;
|
|
82
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Revoke a share the caller owns. Same owner bound (only the owner may unshare).
|
|
97
|
+
* SPEC v3 §5G: NO declared-action check — revoking cannot grant, so a share for an action the
|
|
98
|
+
* type has since stopped declaring stays revocable. An unknown type still gets 400 (§13).
|
|
99
|
+
*/
|
|
83
100
|
unshare(actor: Principal, req: ShareRequest): Promise<void>;
|
|
84
101
|
/**
|
|
85
102
|
* List the shares on a record the caller owns — grantee NAMES resolved (no raw uuid). Only the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"share.service.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"share.service.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ShareService = void 0;
|
|
4
|
+
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
4
5
|
class ShareService {
|
|
5
6
|
writer;
|
|
6
7
|
labels;
|
|
7
|
-
|
|
8
|
+
actionsByType;
|
|
9
|
+
/**
|
|
10
|
+
* @param actionsByType the registration-based action lookup built in `forRoot` (SPEC v2 §5G).
|
|
11
|
+
* Required: share rejects an unknown type or an action the type does not declare (`'*'`
|
|
12
|
+
* stays allowed) with 400 BEFORE the writer is called; unshare rejects only an unknown type
|
|
13
|
+
* (SPEC v3 §5G: revoking cannot grant).
|
|
14
|
+
*/
|
|
15
|
+
constructor(writer, labels, actionsByType) {
|
|
8
16
|
this.writer = writer;
|
|
9
17
|
this.labels = labels;
|
|
18
|
+
this.actionsByType = actionsByType;
|
|
19
|
+
}
|
|
20
|
+
/** 400 on an unknown type; returns the type's declared actions (§5G, §13). */
|
|
21
|
+
assertKnownType(req) {
|
|
22
|
+
// Fail closed: a missing lookup (an untyped JS caller) rejects rather than skipping the check.
|
|
23
|
+
if (typeof this.actionsByType !== 'function') {
|
|
24
|
+
throw new identity_errors_js_1.IdentityBadRequestError(`Type '${req.typeKey}' is not registered.`);
|
|
25
|
+
}
|
|
26
|
+
const declared = this.actionsByType(req.typeKey);
|
|
27
|
+
if (declared === undefined) {
|
|
28
|
+
throw new identity_errors_js_1.IdentityBadRequestError(`Type '${req.typeKey}' is not registered.`);
|
|
29
|
+
}
|
|
30
|
+
return declared;
|
|
31
|
+
}
|
|
32
|
+
/** 400 on an unknown type or an undeclared action — runs before any writer call (§5G). */
|
|
33
|
+
assertDeclaredAction(req) {
|
|
34
|
+
const declared = this.assertKnownType(req);
|
|
35
|
+
if (req.action !== '*' && !declared.includes(req.action)) {
|
|
36
|
+
throw new identity_errors_js_1.IdentityBadRequestError(`Action '${req.action}' is not declared on type '${req.typeKey}'.`);
|
|
37
|
+
}
|
|
10
38
|
}
|
|
11
39
|
/**
|
|
12
40
|
* Share a record the caller owns with a user/role for an action. The tenant + sharer are the
|
|
@@ -14,6 +42,7 @@ class ShareService {
|
|
|
14
42
|
* The writer proves `actor.id` owns the record before writing (fail-closed).
|
|
15
43
|
*/
|
|
16
44
|
async share(actor, req) {
|
|
45
|
+
this.assertDeclaredAction(req);
|
|
17
46
|
await this.writer.share({
|
|
18
47
|
tenantId: actor.tenantId, // SERVER-resolved
|
|
19
48
|
typeKey: req.typeKey,
|
|
@@ -24,8 +53,13 @@ class ShareService {
|
|
|
24
53
|
sharerId: actor.id, // SERVER-resolved — the writer verifies ownership
|
|
25
54
|
});
|
|
26
55
|
}
|
|
27
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Revoke a share the caller owns. Same owner bound (only the owner may unshare).
|
|
58
|
+
* SPEC v3 §5G: NO declared-action check — revoking cannot grant, so a share for an action the
|
|
59
|
+
* type has since stopped declaring stays revocable. An unknown type still gets 400 (§13).
|
|
60
|
+
*/
|
|
28
61
|
async unshare(actor, req) {
|
|
62
|
+
this.assertKnownType(req);
|
|
29
63
|
await this.writer.unshare({
|
|
30
64
|
tenantId: actor.tenantId,
|
|
31
65
|
typeKey: req.typeKey,
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { MembershipPosting } from './membership-posting.object.js';
|
|
|
14
14
|
export { CompanyDepartment } from './company-department.object.js';
|
|
15
15
|
export { CompanyBranch } from './company-branch.object.js';
|
|
16
16
|
export { ServiceAccount } from './service-account.object.js';
|
|
17
|
+
export { StoredFile } from './stored-file.object.js';
|
|
17
18
|
export { identityMigrationsDir } from './migrations-dir.js';
|
|
18
19
|
/**
|
|
19
20
|
* All identity objects, registered with the engine at boot (same set + order as before).
|
|
@@ -29,6 +30,9 @@ export { identityObjects } from './objects.js';
|
|
|
29
30
|
* control-plane keys WITHOUT guard importing this package.
|
|
30
31
|
*/
|
|
31
32
|
export { identityAuthActionKeys, identityControlPlaneKeys } from './auth-facts-keys.js';
|
|
33
|
+
/** KI #70: the same key universe with the consumer's `extraObjects` (pass the SAME list to
|
|
34
|
+
* `IdentityFeatureModule.forRoot({ extraObjects })`). */
|
|
35
|
+
export { buildIdentityAuthKeys, type IdentityAuthKeys } from './auth-facts-keys.js';
|
|
32
36
|
/**
|
|
33
37
|
* The framework-owned identity FEATURE module (company/user/role CRUD). The consumer imports
|
|
34
38
|
* `IdentityFeatureModule.forRoot()` in one line alongside `ShieldModule.forRoot(...)` (ADR-0026);
|
|
@@ -36,6 +40,11 @@ export { identityAuthActionKeys, identityControlPlaneKeys } from './auth-facts-k
|
|
|
36
40
|
* `identityObjects`, and mounts the CRUD controllers — no separate engine setup needed.
|
|
37
41
|
*/
|
|
38
42
|
export { IdentityFeatureModule } from './features/identity-feature.module.js';
|
|
43
|
+
/**
|
|
44
|
+
* Stored files (file-storage SPEC §5 I-K): `store` / `open` / `remove` on the engine pipe, provided
|
|
45
|
+
* and exported by `IdentityFeatureModule`. Apps call it from their own `@Classified` routes.
|
|
46
|
+
*/
|
|
47
|
+
export { FilesService, type StoreFileInput, type StoredFileSummary, type OpenedFile, } from './features/files/files.service.js';
|
|
39
48
|
/**
|
|
40
49
|
* The ADR-0011 per-request session-facts reader (ALS hot path), re-exported so a consumer
|
|
41
50
|
* that wants to construct it directly can do so.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACxF;0DAC0D;AAC1D,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEpF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;GAGG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,mCAAmC,CAAC;AAU3C;;;GAGG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
|