@nextage/nx-frame-be 1.0.44 → 1.0.46
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/build/common/base/__test__/audit.test.d.ts +15 -0
- package/build/common/base/__test__/audit.test.js +126 -0
- package/build/common/base/__test__/before-get-contract.test.d.ts +16 -0
- package/build/common/base/__test__/before-get-contract.test.js +90 -0
- package/build/common/base/__test__/tenancy-opt-out.test.d.ts +21 -0
- package/build/common/base/__test__/tenancy-opt-out.test.js +214 -0
- package/build/common/base/audit.d.ts +55 -0
- package/build/common/base/audit.js +77 -0
- package/build/common/base/base.model.d.ts +67 -2
- package/build/common/base/base.model.js +132 -9
- package/build/common/base/mongo.types.d.ts +17 -0
- package/build/common/base/tenancy.d.ts +101 -0
- package/build/common/base/tenancy.js +66 -0
- package/build/common/crypto/index.d.ts +1 -0
- package/build/common/crypto/index.js +1 -0
- package/build/common/enums.d.ts +17 -0
- package/build/common/enums.js +19 -1
- package/build/common/index.d.ts +2 -0
- package/build/common/index.js +2 -0
- package/build/common/types.d.ts +9 -0
- package/package.json +1 -1
|
@@ -29,6 +29,36 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
|
|
|
29
29
|
* Get default parameters
|
|
30
30
|
*/
|
|
31
31
|
get modelParams(): any;
|
|
32
|
+
/**
|
|
33
|
+
* The tenant scoping in force for THIS model, or `null` when there is none.
|
|
34
|
+
*
|
|
35
|
+
* Read on every call rather than resolved once in the constructor: an application
|
|
36
|
+
* registers its provider during boot, which may well happen after the models have been
|
|
37
|
+
* built, and a value captured too early would be `null` forever.
|
|
38
|
+
*/
|
|
39
|
+
private get tenancy();
|
|
40
|
+
/**
|
|
41
|
+
* Constrains a filter to what this context may read. No-op without tenancy.
|
|
42
|
+
*
|
|
43
|
+
* The field is ALWAYS overwritten, never read: whatever the caller put there is a
|
|
44
|
+
* request, and honouring it would let a client name another tenant simply by adding a
|
|
45
|
+
* field to a query.
|
|
46
|
+
*
|
|
47
|
+
* @param filter the filter, mutated in place
|
|
48
|
+
* @param context the request context
|
|
49
|
+
*/
|
|
50
|
+
protected scopeFilter(filter: FilterParams, context: NxContext): FilterParams;
|
|
51
|
+
/**
|
|
52
|
+
* Asserts this context may write the document it just read. No-op without tenancy.
|
|
53
|
+
*
|
|
54
|
+
* Separate from the read filter, and it has to be: in a `shared` collection everybody
|
|
55
|
+
* reads the shared documents, so a scoped read says nothing about who may change them.
|
|
56
|
+
*
|
|
57
|
+
* @param doc the stored document
|
|
58
|
+
* @param context the request context
|
|
59
|
+
* @throws when the write is not allowed
|
|
60
|
+
*/
|
|
61
|
+
protected assertWritable(doc: NxObject | null, context: NxContext): void;
|
|
32
62
|
/**
|
|
33
63
|
*
|
|
34
64
|
*/
|
|
@@ -96,12 +126,21 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
|
|
|
96
126
|
*/
|
|
97
127
|
get(id: string, context?: NxContext): Promise<TDoc | null>;
|
|
98
128
|
/**
|
|
129
|
+
* Decides whether a subscriber may see THIS event payload.
|
|
130
|
+
*
|
|
131
|
+
* Being authenticated is not being entitled: with tenancy in force the payload is a
|
|
132
|
+
* document, and a document has an owner. Left at "any authenticated user" - as it was -
|
|
133
|
+
* a single channel carried every tenant's documents to every subscriber.
|
|
99
134
|
*
|
|
100
135
|
* @param {*} item
|
|
101
136
|
* @param {*} context
|
|
102
137
|
*/
|
|
103
138
|
checkContextPermission(item: any, context?: NxContext): any;
|
|
104
139
|
/**
|
|
140
|
+
* Decides whether a subscriber may listen on a channel carrying THIS payload.
|
|
141
|
+
*
|
|
142
|
+
* Note the parameter is the payload itself, not an id, whatever its name suggests:
|
|
143
|
+
* `getChannelSubscriber` hands over `payload[payloadId]`.
|
|
105
144
|
*
|
|
106
145
|
* @param {*} context
|
|
107
146
|
*/
|
|
@@ -127,18 +166,37 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
|
|
|
127
166
|
*/
|
|
128
167
|
findOneNative(params: FilterParams): Promise<mongoose.mongo.WithId<mongoose.mongo.BSON.Document> | null>;
|
|
129
168
|
/**
|
|
169
|
+
* Scopes a read by id. Overriding this REPLACES the tenant scoping, it does not add to it.
|
|
130
170
|
*
|
|
131
|
-
* @param
|
|
171
|
+
* @param filter
|
|
132
172
|
* @param context
|
|
133
|
-
* @returns
|
|
173
|
+
* @returns the filter the query must use - the RETURNED one, which is what `get` uses
|
|
134
174
|
*/
|
|
135
175
|
beforeGet(filter: FilterParams, context: NxContext): Promise<FilterParams>;
|
|
136
176
|
/**
|
|
177
|
+
* Stamps the owner and the audit on a new document.
|
|
178
|
+
*
|
|
179
|
+
* The value in the payload is a REQUEST, never an instruction: the provider decides what
|
|
180
|
+
* is actually written, and refuses rather than downgrade when the context is not
|
|
181
|
+
* entitled to the scope it asked for.
|
|
182
|
+
*
|
|
183
|
+
* Declared `async` because that refusal is a THROW, and the signature promises a promise:
|
|
184
|
+
* left synchronous, a caller reaching for `.catch()` would never see it, while one using
|
|
185
|
+
* `await` would - the same split contract that made `beforeGet` silently unsafe.
|
|
186
|
+
*
|
|
187
|
+
* Overriding this to set other fields is expected - denormalised data, defaults - but
|
|
188
|
+
* call `super`, or both the scoping and the audit silently stop happening for this model
|
|
189
|
+
* alone.
|
|
137
190
|
*
|
|
138
191
|
* @param {*} item
|
|
139
192
|
*/
|
|
140
193
|
beforeCreate(item: TAttrs, context: NxContext): Promise<TAttrs>;
|
|
141
194
|
/**
|
|
195
|
+
* Keeps an update from re-homing a document into another tenant, and stamps the audit.
|
|
196
|
+
*
|
|
197
|
+
* The owner is dropped from the payload rather than validated: an update never has a
|
|
198
|
+
* legitimate reason to move a document across tenants, so there is nothing to allow. The
|
|
199
|
+
* creation fields go the same way, for the same reason - a document is created once.
|
|
142
200
|
*
|
|
143
201
|
* @param {*} item
|
|
144
202
|
*/
|
|
@@ -149,6 +207,12 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
|
|
|
149
207
|
*/
|
|
150
208
|
beforeCopy(item: TAttrs, context: NxContext): Promise<TAttrs>;
|
|
151
209
|
/**
|
|
210
|
+
* Asserts the caller may delete this document, BEFORE it is deleted.
|
|
211
|
+
*
|
|
212
|
+
* `remove` deletes by `_id` with no filter whatsoever, and `bulkRemove` is a loop over
|
|
213
|
+
* it: without this check any caller deletes another tenant's documents knowing only an
|
|
214
|
+
* id. The document is re-read unscoped on purpose - the point is to judge its real
|
|
215
|
+
* owner, and a scoped read would hide the very case being guarded against.
|
|
152
216
|
*
|
|
153
217
|
* @param {*} id
|
|
154
218
|
*/
|
|
@@ -179,6 +243,7 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
|
|
|
179
243
|
*/
|
|
180
244
|
manageFilterArrayParams(params: FilterParams): void;
|
|
181
245
|
/**
|
|
246
|
+
* Scopes list / count / findOne / aggregate - the bulk of the read surface.
|
|
182
247
|
*
|
|
183
248
|
* @param {*} params
|
|
184
249
|
* @param {*} context
|
|
@@ -30,6 +30,8 @@ const utils_1 = require("../utils");
|
|
|
30
30
|
const constants_1 = require("../constants");
|
|
31
31
|
const enums_1 = require("../enums");
|
|
32
32
|
const errors_1 = require("../errors");
|
|
33
|
+
const tenancy_1 = require("./tenancy");
|
|
34
|
+
const audit_1 = require("./audit");
|
|
33
35
|
const mongo_utils_1 = require("./mongo-utils");
|
|
34
36
|
class BaseModel extends events_1.EventEmitter {
|
|
35
37
|
constructor({ name, model, params, logger }) {
|
|
@@ -73,6 +75,50 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
73
75
|
return null;
|
|
74
76
|
return (0, utils_1.clone)(this.defaultParams);
|
|
75
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* The tenant scoping in force for THIS model, or `null` when there is none.
|
|
80
|
+
*
|
|
81
|
+
* Read on every call rather than resolved once in the constructor: an application
|
|
82
|
+
* registers its provider during boot, which may well happen after the models have been
|
|
83
|
+
* built, and a value captured too early would be `null` forever.
|
|
84
|
+
*/
|
|
85
|
+
get tenancy() {
|
|
86
|
+
var _a;
|
|
87
|
+
return (0, tenancy_1.resolveTenancy)(constants_1.APP.tenancy, this.schema, (_a = this.defaultParams) === null || _a === void 0 ? void 0 : _a.tenancy);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Constrains a filter to what this context may read. No-op without tenancy.
|
|
91
|
+
*
|
|
92
|
+
* The field is ALWAYS overwritten, never read: whatever the caller put there is a
|
|
93
|
+
* request, and honouring it would let a client name another tenant simply by adding a
|
|
94
|
+
* field to a query.
|
|
95
|
+
*
|
|
96
|
+
* @param filter the filter, mutated in place
|
|
97
|
+
* @param context the request context
|
|
98
|
+
*/
|
|
99
|
+
scopeFilter(filter, context) {
|
|
100
|
+
const tenancy = this.tenancy;
|
|
101
|
+
if (tenancy && filter)
|
|
102
|
+
filter[tenancy.provider.field] = tenancy.provider.read(tenancy.mode, context);
|
|
103
|
+
return filter;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Asserts this context may write the document it just read. No-op without tenancy.
|
|
107
|
+
*
|
|
108
|
+
* Separate from the read filter, and it has to be: in a `shared` collection everybody
|
|
109
|
+
* reads the shared documents, so a scoped read says nothing about who may change them.
|
|
110
|
+
*
|
|
111
|
+
* @param doc the stored document
|
|
112
|
+
* @param context the request context
|
|
113
|
+
* @throws when the write is not allowed
|
|
114
|
+
*/
|
|
115
|
+
assertWritable(doc, context) {
|
|
116
|
+
const tenancy = this.tenancy;
|
|
117
|
+
if (!tenancy || !doc)
|
|
118
|
+
return;
|
|
119
|
+
if (!tenancy.provider.canWrite(tenancy.mode, doc[tenancy.provider.field], context))
|
|
120
|
+
throw new errors_1.ForbiddenError();
|
|
121
|
+
}
|
|
76
122
|
/**
|
|
77
123
|
*
|
|
78
124
|
*/
|
|
@@ -213,25 +259,50 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
213
259
|
return __awaiter(this, arguments, void 0, function* (id, context = {}) {
|
|
214
260
|
if (!id)
|
|
215
261
|
return null;
|
|
216
|
-
|
|
217
|
-
|
|
262
|
+
// The RETURNED filter is the one that counts. This used to discard it and query with
|
|
263
|
+
// the object built here, which worked only because every override mutates its argument:
|
|
264
|
+
// one written in pure style - `return { ...filter, tenantId }`, the shape this very
|
|
265
|
+
// signature invites - compiled, read across every tenant, and said nothing.
|
|
266
|
+
const filter = yield this.beforeGet({ _id: (0, mongo_utils_1.toObjectId)(id) }, context);
|
|
218
267
|
return this.mgModel.findOne(filter, null, { session: context === null || context === void 0 ? void 0 : context.session });
|
|
219
268
|
});
|
|
220
269
|
}
|
|
221
270
|
/**
|
|
271
|
+
* Decides whether a subscriber may see THIS event payload.
|
|
272
|
+
*
|
|
273
|
+
* Being authenticated is not being entitled: with tenancy in force the payload is a
|
|
274
|
+
* document, and a document has an owner. Left at "any authenticated user" - as it was -
|
|
275
|
+
* a single channel carried every tenant's documents to every subscriber.
|
|
222
276
|
*
|
|
223
277
|
* @param {*} item
|
|
224
278
|
* @param {*} context
|
|
225
279
|
*/
|
|
226
280
|
checkContextPermission(item, context = {}) {
|
|
227
|
-
|
|
281
|
+
if (!context.user)
|
|
282
|
+
return Promise.resolve(null);
|
|
283
|
+
const tenancy = this.tenancy;
|
|
284
|
+
if (!tenancy || !item)
|
|
285
|
+
return Promise.resolve(item);
|
|
286
|
+
const allowed = tenancy.provider.read(tenancy.mode, context);
|
|
287
|
+
return Promise.resolve((0, tenancy_1.matchesTenancy)(item[tenancy.provider.field], allowed) ? item : null);
|
|
228
288
|
}
|
|
229
289
|
/**
|
|
290
|
+
* Decides whether a subscriber may listen on a channel carrying THIS payload.
|
|
291
|
+
*
|
|
292
|
+
* Note the parameter is the payload itself, not an id, whatever its name suggests:
|
|
293
|
+
* `getChannelSubscriber` hands over `payload[payloadId]`.
|
|
230
294
|
*
|
|
231
295
|
* @param {*} context
|
|
232
296
|
*/
|
|
233
297
|
checkChannelSubscriber(payloadId, context = {}) {
|
|
234
|
-
|
|
298
|
+
if (!context.user)
|
|
299
|
+
return Promise.resolve(false);
|
|
300
|
+
const tenancy = this.tenancy;
|
|
301
|
+
const payload = payloadId;
|
|
302
|
+
if (!tenancy || !payload || typeof payload !== 'object')
|
|
303
|
+
return Promise.resolve(true);
|
|
304
|
+
const allowed = tenancy.provider.read(tenancy.mode, context);
|
|
305
|
+
return Promise.resolve((0, tenancy_1.matchesTenancy)(payload[tenancy.provider.field], allowed));
|
|
235
306
|
}
|
|
236
307
|
/**
|
|
237
308
|
* Get document using Moongose wrapper method
|
|
@@ -265,28 +336,63 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
265
336
|
return this.mgModel.collection.findOne(params);
|
|
266
337
|
}
|
|
267
338
|
/**
|
|
339
|
+
* Scopes a read by id. Overriding this REPLACES the tenant scoping, it does not add to it.
|
|
268
340
|
*
|
|
269
|
-
* @param
|
|
341
|
+
* @param filter
|
|
270
342
|
* @param context
|
|
271
|
-
* @returns
|
|
343
|
+
* @returns the filter the query must use - the RETURNED one, which is what `get` uses
|
|
272
344
|
*/
|
|
273
345
|
beforeGet(filter, context) {
|
|
274
346
|
return __awaiter(this, void 0, void 0, function* () {
|
|
275
|
-
return filter;
|
|
347
|
+
return this.scopeFilter(filter, context);
|
|
276
348
|
});
|
|
277
349
|
}
|
|
278
350
|
/**
|
|
351
|
+
* Stamps the owner and the audit on a new document.
|
|
352
|
+
*
|
|
353
|
+
* The value in the payload is a REQUEST, never an instruction: the provider decides what
|
|
354
|
+
* is actually written, and refuses rather than downgrade when the context is not
|
|
355
|
+
* entitled to the scope it asked for.
|
|
356
|
+
*
|
|
357
|
+
* Declared `async` because that refusal is a THROW, and the signature promises a promise:
|
|
358
|
+
* left synchronous, a caller reaching for `.catch()` would never see it, while one using
|
|
359
|
+
* `await` would - the same split contract that made `beforeGet` silently unsafe.
|
|
360
|
+
*
|
|
361
|
+
* Overriding this to set other fields is expected - denormalised data, defaults - but
|
|
362
|
+
* call `super`, or both the scoping and the audit silently stop happening for this model
|
|
363
|
+
* alone.
|
|
279
364
|
*
|
|
280
365
|
* @param {*} item
|
|
281
366
|
*/
|
|
282
367
|
beforeCreate(item, context) {
|
|
283
|
-
return
|
|
368
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
369
|
+
var _a;
|
|
370
|
+
const tenancy = this.tenancy;
|
|
371
|
+
if (tenancy && item) {
|
|
372
|
+
const field = tenancy.provider.field;
|
|
373
|
+
item[field] = tenancy.provider.write(tenancy.mode, item[field], context);
|
|
374
|
+
}
|
|
375
|
+
if (((_a = this.defaultParams) === null || _a === void 0 ? void 0 : _a.audit) !== false)
|
|
376
|
+
(0, audit_1.stampAudit)(item, this.schema, context, true);
|
|
377
|
+
return item;
|
|
378
|
+
});
|
|
284
379
|
}
|
|
285
380
|
/**
|
|
381
|
+
* Keeps an update from re-homing a document into another tenant, and stamps the audit.
|
|
382
|
+
*
|
|
383
|
+
* The owner is dropped from the payload rather than validated: an update never has a
|
|
384
|
+
* legitimate reason to move a document across tenants, so there is nothing to allow. The
|
|
385
|
+
* creation fields go the same way, for the same reason - a document is created once.
|
|
286
386
|
*
|
|
287
387
|
* @param {*} item
|
|
288
388
|
*/
|
|
289
389
|
beforeUpdate(item, context) {
|
|
390
|
+
var _a;
|
|
391
|
+
const tenancy = this.tenancy;
|
|
392
|
+
if (tenancy && item)
|
|
393
|
+
delete item[tenancy.provider.field];
|
|
394
|
+
if (((_a = this.defaultParams) === null || _a === void 0 ? void 0 : _a.audit) !== false)
|
|
395
|
+
(0, audit_1.stampAudit)(item, this.schema, context, false);
|
|
290
396
|
return Promise.resolve(item);
|
|
291
397
|
}
|
|
292
398
|
/**
|
|
@@ -297,11 +403,23 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
297
403
|
return Promise.resolve(item);
|
|
298
404
|
}
|
|
299
405
|
/**
|
|
406
|
+
* Asserts the caller may delete this document, BEFORE it is deleted.
|
|
407
|
+
*
|
|
408
|
+
* `remove` deletes by `_id` with no filter whatsoever, and `bulkRemove` is a loop over
|
|
409
|
+
* it: without this check any caller deletes another tenant's documents knowing only an
|
|
410
|
+
* id. The document is re-read unscoped on purpose - the point is to judge its real
|
|
411
|
+
* owner, and a scoped read would hide the very case being guarded against.
|
|
300
412
|
*
|
|
301
413
|
* @param {*} id
|
|
302
414
|
*/
|
|
303
415
|
beforeRemove(id, context) {
|
|
304
|
-
return
|
|
416
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
417
|
+
if (this.tenancy && id) {
|
|
418
|
+
const doc = yield this.mgModel.findOne({ _id: (0, mongo_utils_1.toObjectId)(id) });
|
|
419
|
+
this.assertWritable(doc, context);
|
|
420
|
+
}
|
|
421
|
+
return id;
|
|
422
|
+
});
|
|
305
423
|
}
|
|
306
424
|
/**
|
|
307
425
|
*
|
|
@@ -345,6 +463,7 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
345
463
|
}
|
|
346
464
|
}
|
|
347
465
|
/**
|
|
466
|
+
* Scopes list / count / findOne / aggregate - the bulk of the read surface.
|
|
348
467
|
*
|
|
349
468
|
* @param {*} params
|
|
350
469
|
* @param {*} context
|
|
@@ -354,6 +473,7 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
354
473
|
var _a;
|
|
355
474
|
if (!params)
|
|
356
475
|
params = {};
|
|
476
|
+
this.scopeFilter(params, context);
|
|
357
477
|
this.manageFilterArrayParams(params);
|
|
358
478
|
(_a = params.rangeDate) !== null && _a !== void 0 ? _a : (params.rangeDate = {});
|
|
359
479
|
let and = [];
|
|
@@ -469,6 +589,9 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
469
589
|
const itemDoc = yield this.get(id, context);
|
|
470
590
|
if (!itemDoc)
|
|
471
591
|
throw new errors_1.AppError('messages.notFound');
|
|
592
|
+
// A scoped READ is not a permission to WRITE: in a shared collection the document
|
|
593
|
+
// just read may well be one everybody sees and only the application tier may change.
|
|
594
|
+
this.assertWritable(itemDoc, context);
|
|
472
595
|
let toApply = item;
|
|
473
596
|
// complete merge with the freshly read document
|
|
474
597
|
if (mergeItem) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NxObject } from '../interfaces';
|
|
2
|
+
import { TenancyMode } from '../enums';
|
|
2
3
|
export type ModelDef<TDoc, TMongoModel, TNxModel> = {
|
|
3
4
|
classDef: new (params: ModelData<TDoc, TMongoModel>) => TNxModel;
|
|
4
5
|
name: string;
|
|
@@ -18,4 +19,20 @@ export type ModelData<TDoc, TMongoModel> = {
|
|
|
18
19
|
export type ModelDefParams = {
|
|
19
20
|
sort?: NxObject;
|
|
20
21
|
CRUDEvents?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* How this collection relates to tenants.
|
|
24
|
+
*
|
|
25
|
+
* Omitted, a schema declaring the tenant field is scoped to ONE tenant: the scoping is
|
|
26
|
+
* opt-OUT, so silence means isolated. Say `none` to exempt a collection, and say WHY
|
|
27
|
+
* next to it - an unexplained exemption cannot be told apart from an oversight.
|
|
28
|
+
*/
|
|
29
|
+
tenancy?: TenancyMode;
|
|
30
|
+
/**
|
|
31
|
+
* Whether `createdBy`/`createdAt`/`updatedBy`/`updatedAt` are kept automatically.
|
|
32
|
+
*
|
|
33
|
+
* Omitted, they are - but only for the fields the schema actually declares, so a model
|
|
34
|
+
* that never wanted an audit gets none either way. Set `false` for a collection that
|
|
35
|
+
* declares the fields and fills them itself, which today is nothing.
|
|
36
|
+
*/
|
|
37
|
+
audit?: boolean;
|
|
21
38
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*********************************************************************************************
|
|
2
|
+
* *
|
|
3
|
+
* TENANCY - the contract that lets `BaseModel` scope by tenant WITHOUT knowing what a *
|
|
4
|
+
* tenant is. *
|
|
5
|
+
* *
|
|
6
|
+
* Isolation used to be opt-IN: `BaseModel` scoped nothing, so a model owning a tenant *
|
|
7
|
+
* field was isolated only for as long as somebody remembered to override five hooks in *
|
|
8
|
+
* every single class. Forgetting one is silent - the collection simply answers every *
|
|
9
|
+
* tenant - and it is a mistake that has already been made three times over. *
|
|
10
|
+
* *
|
|
11
|
+
* Here it is opt-OUT: a model whose schema declares the tenant field is scoped by *
|
|
12
|
+
* default, and a model that must NOT be scoped has to say so. *
|
|
13
|
+
* *
|
|
14
|
+
* WHY A PROVIDER, AND NOT THE RULES THEMSELVES *
|
|
15
|
+
* *
|
|
16
|
+
* "Which tenant is this request" is an application question: it depends on roles, on *
|
|
17
|
+
* memberships, on grants - none of which this library knows, nor should. So the library *
|
|
18
|
+
* asks, and the application answers by registering a provider on `APP.tenancy`. *
|
|
19
|
+
* *
|
|
20
|
+
* IMPORTANT: with NO provider registered, every model behaves exactly as it did before *
|
|
21
|
+
* this file existed. The scoping is inert until an application opts into it, which is *
|
|
22
|
+
* what makes the change safe for consumers that know nothing about tenants. *
|
|
23
|
+
* *
|
|
24
|
+
*********************************************************************************************/
|
|
25
|
+
import { NxContext, NxObject } from '../interfaces';
|
|
26
|
+
import { TenancyMode } from '../enums';
|
|
27
|
+
/**
|
|
28
|
+
* What the application must answer for the library to scope on its behalf.
|
|
29
|
+
*
|
|
30
|
+
* Every method takes the mode, so one provider serves every model: the rules of a `shared`
|
|
31
|
+
* collection differ from those of an `own` one, and the difference belongs to the
|
|
32
|
+
* application, not to a second provider.
|
|
33
|
+
*/
|
|
34
|
+
export interface TenancyProvider {
|
|
35
|
+
/** Schema field carrying the owner. */
|
|
36
|
+
field: string;
|
|
37
|
+
/**
|
|
38
|
+
* The value a READ must be constrained to - a plain id for `own`, typically an `$in`
|
|
39
|
+
* over the tenant and the shared marker for `shared`.
|
|
40
|
+
*
|
|
41
|
+
* @param mode the model's declared mode
|
|
42
|
+
* @param context the request context
|
|
43
|
+
* @returns whatever mongo should match the field against
|
|
44
|
+
*/
|
|
45
|
+
read(mode: TenancyMode, context: NxContext): unknown;
|
|
46
|
+
/**
|
|
47
|
+
* The value a NEW document must carry, and the place where a write is authorised.
|
|
48
|
+
*
|
|
49
|
+
* `requested` is whatever the payload carried under the tenant field: it is a REQUEST and
|
|
50
|
+
* never an instruction - honour it only for a context entitled to it, and refuse
|
|
51
|
+
* otherwise rather than silently downgrading, which would let a client discover the rule
|
|
52
|
+
* by trial.
|
|
53
|
+
*
|
|
54
|
+
* @param mode the model's declared mode
|
|
55
|
+
* @param requested the value found in the payload, when any
|
|
56
|
+
* @param context the request context
|
|
57
|
+
* @returns the value to stamp on the document
|
|
58
|
+
* @throws when the context may not write the requested scope
|
|
59
|
+
*/
|
|
60
|
+
write(mode: TenancyMode, requested: unknown, context: NxContext): unknown;
|
|
61
|
+
/**
|
|
62
|
+
* Whether an EXISTING document may be modified or deleted by this context.
|
|
63
|
+
*
|
|
64
|
+
* Separate from `read` on purpose: in a `shared` collection everybody reads the shared
|
|
65
|
+
* documents and only the application tier writes them, so a filter alone - which is a
|
|
66
|
+
* read - cannot express the rule.
|
|
67
|
+
*
|
|
68
|
+
* @param mode the model's declared mode
|
|
69
|
+
* @param current the value the stored document carries
|
|
70
|
+
* @param context the request context
|
|
71
|
+
*/
|
|
72
|
+
canWrite(mode: TenancyMode, current: unknown, context: NxContext): boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Reads the tenancy in force for a model, or `null` when there is none.
|
|
76
|
+
*
|
|
77
|
+
* Three conditions, all necessary: an application must have registered a provider, the
|
|
78
|
+
* model must not have opted out, and the schema must actually declare the field - a model
|
|
79
|
+
* without it has no owner to scope by.
|
|
80
|
+
*
|
|
81
|
+
* @param provider the registered provider, when any
|
|
82
|
+
* @param schema the model's mongoose schema definition
|
|
83
|
+
* @param declared the mode declared in `ModelDefParams`, when any
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveTenancy(provider: TenancyProvider | undefined, schema: NxObject | undefined, declared?: TenancyMode): {
|
|
86
|
+
provider: TenancyProvider;
|
|
87
|
+
mode: TenancyMode;
|
|
88
|
+
} | null;
|
|
89
|
+
/**
|
|
90
|
+
* Whether a document's owner satisfies what a read allows.
|
|
91
|
+
*
|
|
92
|
+
* The allowed value is whatever `TenancyProvider.read` returned, so it is either a plain
|
|
93
|
+
* value or a mongo operator. Only `$in` is understood, and deliberately so: this is used
|
|
94
|
+
* where no query runs - filtering an event payload in memory - and quietly accepting an
|
|
95
|
+
* operator it cannot evaluate would let a document through unchecked. Anything else is
|
|
96
|
+
* refused, which fails closed.
|
|
97
|
+
*
|
|
98
|
+
* @param current the value carried by the document
|
|
99
|
+
* @param allowed what `read` returned for this context
|
|
100
|
+
*/
|
|
101
|
+
export declare function matchesTenancy(current: unknown, allowed: unknown): boolean;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*********************************************************************************************
|
|
3
|
+
* *
|
|
4
|
+
* TENANCY - the contract that lets `BaseModel` scope by tenant WITHOUT knowing what a *
|
|
5
|
+
* tenant is. *
|
|
6
|
+
* *
|
|
7
|
+
* Isolation used to be opt-IN: `BaseModel` scoped nothing, so a model owning a tenant *
|
|
8
|
+
* field was isolated only for as long as somebody remembered to override five hooks in *
|
|
9
|
+
* every single class. Forgetting one is silent - the collection simply answers every *
|
|
10
|
+
* tenant - and it is a mistake that has already been made three times over. *
|
|
11
|
+
* *
|
|
12
|
+
* Here it is opt-OUT: a model whose schema declares the tenant field is scoped by *
|
|
13
|
+
* default, and a model that must NOT be scoped has to say so. *
|
|
14
|
+
* *
|
|
15
|
+
* WHY A PROVIDER, AND NOT THE RULES THEMSELVES *
|
|
16
|
+
* *
|
|
17
|
+
* "Which tenant is this request" is an application question: it depends on roles, on *
|
|
18
|
+
* memberships, on grants - none of which this library knows, nor should. So the library *
|
|
19
|
+
* asks, and the application answers by registering a provider on `APP.tenancy`. *
|
|
20
|
+
* *
|
|
21
|
+
* IMPORTANT: with NO provider registered, every model behaves exactly as it did before *
|
|
22
|
+
* this file existed. The scoping is inert until an application opts into it, which is *
|
|
23
|
+
* what makes the change safe for consumers that know nothing about tenants. *
|
|
24
|
+
* *
|
|
25
|
+
*********************************************************************************************/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.resolveTenancy = resolveTenancy;
|
|
28
|
+
exports.matchesTenancy = matchesTenancy;
|
|
29
|
+
const enums_1 = require("../enums");
|
|
30
|
+
/**
|
|
31
|
+
* Reads the tenancy in force for a model, or `null` when there is none.
|
|
32
|
+
*
|
|
33
|
+
* Three conditions, all necessary: an application must have registered a provider, the
|
|
34
|
+
* model must not have opted out, and the schema must actually declare the field - a model
|
|
35
|
+
* without it has no owner to scope by.
|
|
36
|
+
*
|
|
37
|
+
* @param provider the registered provider, when any
|
|
38
|
+
* @param schema the model's mongoose schema definition
|
|
39
|
+
* @param declared the mode declared in `ModelDefParams`, when any
|
|
40
|
+
*/
|
|
41
|
+
function resolveTenancy(provider, schema, declared) {
|
|
42
|
+
if (!provider || declared === enums_1.TenancyMode.none)
|
|
43
|
+
return null;
|
|
44
|
+
if (!schema || !(provider.field in schema))
|
|
45
|
+
return null;
|
|
46
|
+
return { provider, mode: declared !== null && declared !== void 0 ? declared : enums_1.TenancyMode.own };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether a document's owner satisfies what a read allows.
|
|
50
|
+
*
|
|
51
|
+
* The allowed value is whatever `TenancyProvider.read` returned, so it is either a plain
|
|
52
|
+
* value or a mongo operator. Only `$in` is understood, and deliberately so: this is used
|
|
53
|
+
* where no query runs - filtering an event payload in memory - and quietly accepting an
|
|
54
|
+
* operator it cannot evaluate would let a document through unchecked. Anything else is
|
|
55
|
+
* refused, which fails closed.
|
|
56
|
+
*
|
|
57
|
+
* @param current the value carried by the document
|
|
58
|
+
* @param allowed what `read` returned for this context
|
|
59
|
+
*/
|
|
60
|
+
function matchesTenancy(current, allowed) {
|
|
61
|
+
if (allowed && typeof allowed === 'object') {
|
|
62
|
+
const values = allowed.$in;
|
|
63
|
+
return Array.isArray(values) ? values.includes(current) : false;
|
|
64
|
+
}
|
|
65
|
+
return current === allowed;
|
|
66
|
+
}
|
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./crypto.interfaces"), exports);
|
|
18
|
+
__exportStar(require("./crypto.constants"), exports);
|
|
18
19
|
__exportStar(require("./crypto.classes"), exports);
|
|
19
20
|
__exportStar(require("./crypto.utils"), exports);
|
|
20
21
|
__exportStar(require("./crypto-legacy"), exports);
|
package/build/common/enums.d.ts
CHANGED
|
@@ -59,3 +59,20 @@ export declare enum AuthIssuer {
|
|
|
59
59
|
USER = "user",
|
|
60
60
|
DEVICE = "device"
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* How a collection relates to tenants. Declared per model in `ModelDefParams.tenancy`.
|
|
64
|
+
*
|
|
65
|
+
* - `own` every document belongs to exactly ONE tenant. The default whenever the schema
|
|
66
|
+
* declares the tenant field, because it is the safe reading of a tenant field:
|
|
67
|
+
* assuming otherwise is what leaks.
|
|
68
|
+
* - `shared` the collection holds BOTH documents shared by all tenants and documents owned
|
|
69
|
+
* by one. Reads span the two, writes do not: see `TenancyProvider.write`.
|
|
70
|
+
* - `none` the collection carries the field but is NOT request-scoped. Always a decision,
|
|
71
|
+
* never a default - state the reason next to it, because an unexplained `none`
|
|
72
|
+
* is indistinguishable from an oversight.
|
|
73
|
+
*/
|
|
74
|
+
export declare enum TenancyMode {
|
|
75
|
+
own = "own",
|
|
76
|
+
shared = "shared",
|
|
77
|
+
none = "none"
|
|
78
|
+
}
|
package/build/common/enums.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AuthIssuer = exports.ValueType = exports.DatePeriod = exports.ModelCrudEventType = exports.AppStatus = exports.AppMode = void 0;
|
|
3
|
+
exports.TenancyMode = exports.AuthIssuer = exports.ValueType = exports.DatePeriod = exports.ModelCrudEventType = exports.AppStatus = exports.AppMode = void 0;
|
|
4
4
|
var AppMode;
|
|
5
5
|
(function (AppMode) {
|
|
6
6
|
AppMode["DEV"] = "development";
|
|
@@ -73,3 +73,21 @@ var AuthIssuer;
|
|
|
73
73
|
AuthIssuer["USER"] = "user";
|
|
74
74
|
AuthIssuer["DEVICE"] = "device";
|
|
75
75
|
})(AuthIssuer || (exports.AuthIssuer = AuthIssuer = {}));
|
|
76
|
+
/**
|
|
77
|
+
* How a collection relates to tenants. Declared per model in `ModelDefParams.tenancy`.
|
|
78
|
+
*
|
|
79
|
+
* - `own` every document belongs to exactly ONE tenant. The default whenever the schema
|
|
80
|
+
* declares the tenant field, because it is the safe reading of a tenant field:
|
|
81
|
+
* assuming otherwise is what leaks.
|
|
82
|
+
* - `shared` the collection holds BOTH documents shared by all tenants and documents owned
|
|
83
|
+
* by one. Reads span the two, writes do not: see `TenancyProvider.write`.
|
|
84
|
+
* - `none` the collection carries the field but is NOT request-scoped. Always a decision,
|
|
85
|
+
* never a default - state the reason next to it, because an unexplained `none`
|
|
86
|
+
* is indistinguishable from an oversight.
|
|
87
|
+
*/
|
|
88
|
+
var TenancyMode;
|
|
89
|
+
(function (TenancyMode) {
|
|
90
|
+
TenancyMode["own"] = "own";
|
|
91
|
+
TenancyMode["shared"] = "shared";
|
|
92
|
+
TenancyMode["none"] = "none";
|
|
93
|
+
})(TenancyMode || (exports.TenancyMode = TenancyMode = {}));
|
package/build/common/index.d.ts
CHANGED
package/build/common/index.js
CHANGED
|
@@ -22,6 +22,8 @@ __exportStar(require("./interfaces"), exports);
|
|
|
22
22
|
__exportStar(require("./types"), exports);
|
|
23
23
|
__exportStar(require("./manager"), exports);
|
|
24
24
|
__exportStar(require("./utils"), exports);
|
|
25
|
+
__exportStar(require("./base/tenancy"), exports);
|
|
26
|
+
__exportStar(require("./base/audit"), exports);
|
|
25
27
|
// export * from './models';
|
|
26
28
|
__exportStar(require("./express"), exports);
|
|
27
29
|
// export * from './nx-disk-storage';
|
package/build/common/types.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { NxObject, NxTypeObject } from './interfaces';
|
|
|
9
9
|
import { PubSubManager } from './manager/pubsub-manager';
|
|
10
10
|
import { ShutdownManager } from './manager/shutdown-manager';
|
|
11
11
|
import { CryptoManager } from './manager/crypto-manager';
|
|
12
|
+
import { TenancyProvider } from './base/tenancy';
|
|
12
13
|
export type NxDateRage = {
|
|
13
14
|
from?: Date;
|
|
14
15
|
to?: Date;
|
|
@@ -32,6 +33,14 @@ export type AppData = {
|
|
|
32
33
|
cryptoMng?: CryptoManager;
|
|
33
34
|
streams: ChangeStreamData[];
|
|
34
35
|
config: AppConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Tenant scoping, registered by the application at boot.
|
|
38
|
+
*
|
|
39
|
+
* Left unset - as every consumer that knows nothing about tenants leaves it - the models
|
|
40
|
+
* behave exactly as they did before tenancy existed: the scoping is inert until somebody
|
|
41
|
+
* opts into it.
|
|
42
|
+
*/
|
|
43
|
+
tenancy?: TenancyProvider;
|
|
35
44
|
};
|
|
36
45
|
export type AppConfig = {
|
|
37
46
|
endpoint: AppEndpoint;
|