@flowerforce/flowerbase 1.2.0 → 1.2.1-beta.2
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/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +3 -0
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +5 -2
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +7 -10
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +3 -2
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -1
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +28 -2
- package/dist/features/rules/utils.d.ts.map +1 -1
- package/dist/features/rules/utils.js +11 -2
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +52 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -9
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +540 -483
- package/dist/services/mongodb-atlas/utils.d.ts +9 -2
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +113 -23
- package/dist/shared/handleUserRegistration.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.js +1 -0
- package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
- package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
- package/dist/utils/context/helpers.d.ts +6 -5
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/helpers.js +3 -0
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +2 -0
- package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +11 -4
- package/dist/utils/initializer/registerPlugins.d.ts +3 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +9 -6
- package/dist/utils/roles/helpers.js +9 -2
- package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
- package/dist/utils/roles/machines/commonValidators.js +10 -6
- package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
- package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/B/validators.js +8 -0
- package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/C/index.js +10 -7
- package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
- package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/C/validators.js +29 -0
- package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/D/index.js +13 -11
- package/dist/utils/rules.d.ts +1 -1
- package/dist/utils/rules.d.ts.map +1 -1
- package/dist/utils/rules.js +26 -17
- package/jest.config.ts +2 -12
- package/jest.setup.ts +28 -0
- package/package.json +1 -1
- package/src/auth/controller.ts +3 -0
- package/src/auth/providers/custom-function/controller.ts +5 -2
- package/src/auth/providers/local-userpass/controller.ts +13 -10
- package/src/auth/utils.ts +6 -3
- package/src/constants.ts +7 -2
- package/src/fastify.d.ts +32 -15
- package/src/features/functions/controller.ts +36 -2
- package/src/features/rules/utils.ts +11 -2
- package/src/features/triggers/utils.ts +59 -2
- package/src/index.ts +21 -8
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
- package/src/services/mongodb-atlas/index.ts +143 -90
- package/src/services/mongodb-atlas/utils.ts +158 -22
- package/src/shared/handleUserRegistration.ts +3 -3
- package/src/shared/models/handleUserRegistration.model.ts +8 -3
- package/src/types/fastify-raw-body.d.ts +22 -0
- package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
- package/src/utils/__tests__/registerPlugins.test.ts +16 -1
- package/src/utils/context/helpers.ts +3 -0
- package/src/utils/context/index.ts +1 -0
- package/src/utils/initializer/exposeRoutes.ts +15 -8
- package/src/utils/initializer/registerPlugins.ts +15 -7
- package/src/utils/roles/helpers.ts +20 -3
- package/src/utils/roles/machines/commonValidators.ts +10 -5
- package/src/utils/roles/machines/read/B/validators.ts +8 -0
- package/src/utils/roles/machines/read/C/index.ts +11 -7
- package/src/utils/roles/machines/read/C/validators.ts +21 -0
- package/src/utils/roles/machines/read/D/index.ts +22 -12
- package/src/utils/rules.ts +31 -22
- package/tsconfig.spec.json +7 -0
|
@@ -13,524 +13,581 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const isEqual_1 = __importDefault(require("lodash/isEqual"));
|
|
16
|
-
const mongodb_1 = require("mongodb");
|
|
17
16
|
const machines_1 = require("../../utils/roles/machines");
|
|
18
17
|
const utils_1 = require("../../utils/roles/machines/utils");
|
|
19
18
|
const model_1 = require("./model");
|
|
20
19
|
const utils_2 = require("./utils");
|
|
21
20
|
//TODO aggiungere no-sql inject security
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
// System mode: bypass access control
|
|
100
|
-
return collection.deleteOne(query);
|
|
101
|
-
}),
|
|
102
|
-
/**
|
|
103
|
-
* Inserts a single document into a MongoDB collection with optional role-based validation.
|
|
104
|
-
*
|
|
105
|
-
* @param {OptionalId<Document>} data - The document to insert.
|
|
106
|
-
* @param {InsertOneOptions} [options] - Optional settings for the insert operation, such as `writeConcern`.
|
|
107
|
-
* @returns {Promise<InsertOneResult<Document>>} A promise resolving to the result of the insert operation.
|
|
108
|
-
*
|
|
109
|
-
* @throws {Error} If the user is not authorized to insert the document.
|
|
110
|
-
*
|
|
111
|
-
* @description
|
|
112
|
-
* If `run_as_system` is enabled, the document is inserted directly without any validation.
|
|
113
|
-
* Otherwise:
|
|
114
|
-
* - Determines the appropriate user role using `getWinningRole`.
|
|
115
|
-
* - Validates the insert operation using `checkValidation`.
|
|
116
|
-
* - If validation fails, an error is thrown.
|
|
117
|
-
* - If validation passes, the document is inserted.
|
|
118
|
-
*
|
|
119
|
-
* This ensures that only users with the correct permissions can insert data into the collection.
|
|
120
|
-
*/
|
|
121
|
-
insertOne: (data, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
122
|
-
const { roles } = rules[collName] || {};
|
|
123
|
-
if (!run_as_system) {
|
|
124
|
-
(0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
|
|
125
|
-
const winningRole = (0, utils_1.getWinningRole)(data, user, roles);
|
|
126
|
-
const { status, document } = winningRole
|
|
127
|
-
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
128
|
-
type: 'insert',
|
|
129
|
-
roles,
|
|
130
|
-
cursor: data,
|
|
131
|
-
expansions: {}
|
|
132
|
-
}, user)
|
|
133
|
-
: { status: true, document: data };
|
|
134
|
-
if (!status || !(0, isEqual_1.default)(data, document)) {
|
|
135
|
-
throw new Error('Insert not permitted');
|
|
136
|
-
}
|
|
137
|
-
return collection.insertOne(data, options);
|
|
138
|
-
}
|
|
139
|
-
// System mode: insert without validation
|
|
140
|
-
return collection.insertOne(data, options);
|
|
141
|
-
}),
|
|
142
|
-
/**
|
|
143
|
-
* Updates a single document in a MongoDB collection with optional role-based validation.
|
|
144
|
-
*
|
|
145
|
-
* @param {Filter<Document>} query - The MongoDB query used to match the document to update.
|
|
146
|
-
* @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
|
|
147
|
-
* @param {UpdateOptions} [options] - Optional settings for the update operation.
|
|
148
|
-
* @returns {Promise<UpdateResult>} A promise resolving to the result of the update operation.
|
|
149
|
-
*
|
|
150
|
-
* @throws {Error} If the user is not authorized to update the document.
|
|
151
|
-
*
|
|
152
|
-
* @description
|
|
153
|
-
* If `run_as_system` is enabled, the function directly updates the document using `collection.updateOne(query, data, options)`.
|
|
154
|
-
* Otherwise, it follows these steps:
|
|
155
|
-
* - Applies access control filters to the query using `getFormattedQuery`.
|
|
156
|
-
* - Retrieves the document using `findOne` to check if it exists and whether the user has permission to modify it.
|
|
157
|
-
* - Determines the user's role via `getWinningRole`.
|
|
158
|
-
* - Flattens update operators (`$set`, `$inc`, etc.) if present to extract the final modified fields.
|
|
159
|
-
* - Validates the update data using `checkValidation` to ensure compliance with role-based rules.
|
|
160
|
-
* - Ensures that no unauthorized modifications occur by comparing the validated document with the intended changes.
|
|
161
|
-
* - If validation fails, throws an error; otherwise, updates the document.
|
|
162
|
-
*/
|
|
163
|
-
updateOne: (query, data, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
164
|
-
if (!run_as_system) {
|
|
165
|
-
(0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.UPDATE);
|
|
166
|
-
const { filters, roles } = rules[collName] || {};
|
|
167
|
-
// Apply access control filters
|
|
168
|
-
// Normalize _id
|
|
169
|
-
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
170
|
-
const safeQuery = Array.isArray(formattedQuery)
|
|
171
|
-
? (0, utils_2.normalizeQuery)(formattedQuery)
|
|
172
|
-
: formattedQuery;
|
|
173
|
-
const result = yield collection.findOne({ $and: safeQuery });
|
|
174
|
-
if (!result) {
|
|
175
|
-
throw new Error('Update not permitted');
|
|
176
|
-
}
|
|
177
|
-
const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
|
|
178
|
-
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
179
|
-
const hasOperators = Object.keys(data).some((key) => key.startsWith('$'));
|
|
180
|
-
// Flatten the update object to extract the actual fields being modified
|
|
181
|
-
// const docToCheck = hasOperators
|
|
182
|
-
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
183
|
-
// : data
|
|
184
|
-
const [matchQuery] = formattedQuery; // TODO da chiedere/capire perchè è solo uno. tutti gli altri { $match: { $and: formattedQuery } }
|
|
185
|
-
const pipeline = [
|
|
186
|
-
{
|
|
187
|
-
$match: matchQuery
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
$limit: 1
|
|
191
|
-
},
|
|
192
|
-
...Object.entries(data).map(([key, value]) => ({ [key]: value }))
|
|
193
|
-
];
|
|
194
|
-
const [docToCheck] = hasOperators
|
|
195
|
-
? yield collection.aggregate(pipeline).toArray()
|
|
196
|
-
: [data];
|
|
197
|
-
// Validate update permissions
|
|
198
|
-
const { status, document } = winningRole
|
|
199
|
-
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
200
|
-
type: 'write',
|
|
201
|
-
roles,
|
|
202
|
-
cursor: docToCheck,
|
|
203
|
-
expansions: {}
|
|
204
|
-
}, user)
|
|
205
|
-
: { status: true, document: docToCheck };
|
|
206
|
-
// Ensure no unauthorized changes are made
|
|
207
|
-
const areDocumentsEqual = (0, isEqual_1.default)(document, docToCheck);
|
|
208
|
-
if (!status || !areDocumentsEqual) {
|
|
209
|
-
throw new Error('Update not permitted');
|
|
210
|
-
}
|
|
211
|
-
return collection.updateOne({ $and: formattedQuery }, data, options);
|
|
212
|
-
}
|
|
213
|
-
return collection.updateOne(query, data, options);
|
|
214
|
-
}),
|
|
215
|
-
/**
|
|
216
|
-
* Finds documents in a MongoDB collection with optional role-based access control and post-query validation.
|
|
217
|
-
*
|
|
218
|
-
* @param {Filter<Document>} query - The MongoDB query to filter documents.
|
|
219
|
-
* @returns {FindCursor} A customized `FindCursor` that includes additional access control logic in its `toArray()` method.
|
|
220
|
-
*
|
|
221
|
-
* @description
|
|
222
|
-
* If `run_as_system` is enabled, the function simply returns a regular MongoDB cursor (`collection.find(query)`).
|
|
223
|
-
* Otherwise:
|
|
224
|
-
* - Combines the user query with role-based filters via `getFormattedQuery`.
|
|
225
|
-
* - Executes the query using `collection.find` with a `$and` of all filters.
|
|
226
|
-
* - Returns a cloned `FindCursor` where `toArray()`:
|
|
227
|
-
* - Applies additional post-query validation using `checkValidation` for each document.
|
|
228
|
-
* - Filters out documents the current user is not authorized to read.
|
|
229
|
-
*
|
|
230
|
-
* This ensures that both pre-query filtering and post-query validation are applied consistently.
|
|
231
|
-
*/
|
|
232
|
-
find: (query) => {
|
|
233
|
-
if (!run_as_system) {
|
|
234
|
-
(0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
235
|
-
const { filters, roles } = rules[collName] || {};
|
|
236
|
-
// Pre-query filtering based on access control rules
|
|
237
|
-
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
238
|
-
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {};
|
|
239
|
-
// aggiunto filter per evitare questo errore: $and argument's entries must be objects
|
|
240
|
-
const originalCursor = collection.find(currentQuery);
|
|
241
|
-
// Clone the cursor to override `toArray` with post-query validation
|
|
242
|
-
const client = originalCursor['client'];
|
|
243
|
-
const newCursor = new mongodb_1.FindCursor(client);
|
|
244
|
-
/**
|
|
245
|
-
* Overridden `toArray` method that validates each document for read access.
|
|
246
|
-
*
|
|
247
|
-
* @returns {Promise<Document[]>} An array of documents the user is authorized to read.
|
|
248
|
-
*/
|
|
249
|
-
newCursor.toArray = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
250
|
-
const response = yield originalCursor.toArray();
|
|
251
|
-
const filteredResponse = yield Promise.all(response.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
252
|
-
const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
|
|
253
|
-
const { status, document } = winningRole
|
|
254
|
-
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
255
|
-
type: 'read',
|
|
256
|
-
roles,
|
|
257
|
-
cursor: currentDoc,
|
|
258
|
-
expansions: {}
|
|
259
|
-
}, user)
|
|
260
|
-
: { status: !roles.length, document: currentDoc };
|
|
261
|
-
return status ? document : undefined;
|
|
262
|
-
})));
|
|
263
|
-
return filteredResponse.filter(Boolean);
|
|
264
|
-
});
|
|
265
|
-
return newCursor;
|
|
266
|
-
}
|
|
267
|
-
// System mode: return original unfiltered cursor
|
|
268
|
-
return collection.find(query);
|
|
269
|
-
},
|
|
270
|
-
/**
|
|
271
|
-
* Watches changes on a MongoDB collection with optional role-based filtering of change events.
|
|
272
|
-
*
|
|
273
|
-
* @param {Document[]} [pipeline=[]] - Optional aggregation pipeline stages to apply to the change stream.
|
|
274
|
-
* @param {ChangeStreamOptions} [options] - Optional settings for the change stream, such as `fullDocument`, `resumeAfter`, etc.
|
|
275
|
-
* @returns {ChangeStream} A MongoDB `ChangeStream` instance, optionally enhanced with access control.
|
|
276
|
-
*
|
|
277
|
-
* @description
|
|
278
|
-
* If `run_as_system` is enabled, this function simply returns `collection.watch(pipeline, options)`.
|
|
279
|
-
* Otherwise:
|
|
280
|
-
* - Applies access control filters via `getFormattedQuery`.
|
|
281
|
-
* - Prepends a `$match` stage to the pipeline to limit watched changes to authorized documents.
|
|
282
|
-
* - Overrides the `.on()` method of the returned `ChangeStream` to:
|
|
283
|
-
* - Validate the `fullDocument` and any `updatedFields` using `checkValidation`.
|
|
284
|
-
* - Filter out change events the user is not authorized to see.
|
|
285
|
-
* - Pass only validated and filtered events to the original listener.
|
|
286
|
-
*
|
|
287
|
-
* This allows fine-grained control over what change events a user can observe, based on roles and filters.
|
|
288
|
-
*/
|
|
289
|
-
watch: (pipeline = [], options) => {
|
|
290
|
-
if (!run_as_system) {
|
|
291
|
-
(0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
292
|
-
const { filters, roles } = rules[collName] || {};
|
|
293
|
-
// Apply access filters to initial change stream pipeline
|
|
294
|
-
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, {}, user);
|
|
295
|
-
const firstStep = formattedQuery.length ? {
|
|
296
|
-
$match: {
|
|
297
|
-
$and: formattedQuery
|
|
298
|
-
}
|
|
299
|
-
} : undefined;
|
|
300
|
-
const formattedPipeline = [
|
|
301
|
-
firstStep,
|
|
302
|
-
...pipeline
|
|
303
|
-
].filter(Boolean);
|
|
304
|
-
const result = collection.watch(formattedPipeline, options);
|
|
305
|
-
const originalOn = result.on.bind(result);
|
|
306
|
-
/**
|
|
307
|
-
* Validates a change event against the user's roles.
|
|
308
|
-
*
|
|
309
|
-
* @param {Document} change - A change event from the ChangeStream.
|
|
310
|
-
* @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
|
|
311
|
-
*/
|
|
312
|
-
const isValidChange = (_a) => __awaiter(void 0, [_a], void 0, function* ({ fullDocument, updateDescription }) {
|
|
313
|
-
const winningRole = (0, utils_1.getWinningRole)(fullDocument, user, roles);
|
|
21
|
+
const debugRules = process.env.DEBUG_RULES === 'true';
|
|
22
|
+
const debugServices = process.env.DEBUG_SERVICES === 'true';
|
|
23
|
+
const logDebug = (message, payload) => {
|
|
24
|
+
if (!debugRules)
|
|
25
|
+
return;
|
|
26
|
+
const formatted = payload && typeof payload === 'object' ? JSON.stringify(payload) : payload;
|
|
27
|
+
console.log(`[rules-debug] ${message}`, formatted !== null && formatted !== void 0 ? formatted : '');
|
|
28
|
+
};
|
|
29
|
+
const getUserId = (user) => {
|
|
30
|
+
if (!user || typeof user !== 'object')
|
|
31
|
+
return undefined;
|
|
32
|
+
return user.id;
|
|
33
|
+
};
|
|
34
|
+
const logService = (message, payload) => {
|
|
35
|
+
if (!debugServices)
|
|
36
|
+
return;
|
|
37
|
+
console.log('[service-debug]', message, payload !== null && payload !== void 0 ? payload : '');
|
|
38
|
+
};
|
|
39
|
+
const getOperators = (collection, { rules, collName, user, run_as_system }) => {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
const normalizedRules = rules !== null && rules !== void 0 ? rules : {};
|
|
42
|
+
const collectionRules = normalizedRules[collName];
|
|
43
|
+
const filters = (_a = collectionRules === null || collectionRules === void 0 ? void 0 : collectionRules.filters) !== null && _a !== void 0 ? _a : [];
|
|
44
|
+
const roles = (_b = collectionRules === null || collectionRules === void 0 ? void 0 : collectionRules.roles) !== null && _b !== void 0 ? _b : [];
|
|
45
|
+
const fallbackAccess = (doc = undefined) => ({
|
|
46
|
+
status: false,
|
|
47
|
+
document: doc
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
/**
|
|
51
|
+
* Finds a single document in a MongoDB collection with optional role-based filtering and validation.
|
|
52
|
+
*
|
|
53
|
+
* @param {Filter<Document>} query - The MongoDB query used to match the document.
|
|
54
|
+
* @returns {Promise<Document | {} | null>} A promise resolving to the document if found and permitted, an empty object if access is denied, or `null` if not found.
|
|
55
|
+
*
|
|
56
|
+
* @description
|
|
57
|
+
* If `run_as_system` is enabled, the function behaves like a standard `collection.findOne(query)` with no access checks.
|
|
58
|
+
* Otherwise:
|
|
59
|
+
* - Merges the provided query with any access control filters using `getFormattedQuery`.
|
|
60
|
+
* - Attempts to find the document using the formatted query.
|
|
61
|
+
* - Determines the user's role via `getWinningRole`.
|
|
62
|
+
* - Validates the result using `checkValidation` to ensure read permission.
|
|
63
|
+
* - If validation fails, returns an empty object; otherwise returns the validated document.
|
|
64
|
+
*/
|
|
65
|
+
findOne: (query) => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
|
+
var _a;
|
|
67
|
+
if (!run_as_system) {
|
|
68
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
69
|
+
// Apply access control filters to the query
|
|
70
|
+
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
71
|
+
logDebug('update formattedQuery', {
|
|
72
|
+
collection: collName,
|
|
73
|
+
query,
|
|
74
|
+
formattedQuery
|
|
75
|
+
});
|
|
76
|
+
logDebug('find formattedQuery', {
|
|
77
|
+
collection: collName,
|
|
78
|
+
query,
|
|
79
|
+
formattedQuery,
|
|
80
|
+
rolesLength: roles.length
|
|
81
|
+
});
|
|
82
|
+
logService('findOne query', { collName, formattedQuery });
|
|
83
|
+
const safeQuery = (0, utils_2.normalizeQuery)(formattedQuery);
|
|
84
|
+
logService('findOne normalizedQuery', { collName, safeQuery });
|
|
85
|
+
const result = yield collection.findOne({ $and: safeQuery });
|
|
86
|
+
logDebug('findOne result', {
|
|
87
|
+
collection: collName,
|
|
88
|
+
result
|
|
89
|
+
});
|
|
90
|
+
logService('findOne result', { collName, result });
|
|
91
|
+
const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
|
|
92
|
+
logDebug('findOne winningRole', {
|
|
93
|
+
collection: collName,
|
|
94
|
+
winningRoleName: (_a = winningRole === null || winningRole === void 0 ? void 0 : winningRole.name) !== null && _a !== void 0 ? _a : null,
|
|
95
|
+
userId: getUserId(user)
|
|
96
|
+
});
|
|
314
97
|
const { status, document } = winningRole
|
|
315
98
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
316
99
|
type: 'read',
|
|
317
100
|
roles,
|
|
318
|
-
cursor:
|
|
101
|
+
cursor: result,
|
|
319
102
|
expansions: {}
|
|
320
103
|
}, user)
|
|
321
|
-
:
|
|
322
|
-
|
|
104
|
+
: fallbackAccess(result);
|
|
105
|
+
// Return validated document or empty object if not permitted
|
|
106
|
+
return Promise.resolve(status ? document : {});
|
|
107
|
+
}
|
|
108
|
+
// System mode: no validation applied
|
|
109
|
+
return collection.findOne(query);
|
|
110
|
+
}),
|
|
111
|
+
/**
|
|
112
|
+
* Deletes a single document from a MongoDB collection with optional role-based validation.
|
|
113
|
+
*
|
|
114
|
+
* @param {Filter<Document>} [query={}] - The MongoDB query used to match the document to delete.
|
|
115
|
+
* @returns {Promise<DeleteResult>} A promise resolving to the result of the delete operation.
|
|
116
|
+
*
|
|
117
|
+
* @throws {Error} If the user is not authorized to delete the document.
|
|
118
|
+
*
|
|
119
|
+
* @description
|
|
120
|
+
* If `run_as_system` is enabled, the function deletes the document directly using `collection.deleteOne(query)`.
|
|
121
|
+
* Otherwise:
|
|
122
|
+
* - Applies role-based and custom filters to the query using `getFormattedQuery`.
|
|
123
|
+
* - Retrieves the document using `findOne` to validate user permissions.
|
|
124
|
+
* - Checks if the user has the appropriate role to perform a delete via `checkValidation`.
|
|
125
|
+
* - If validation fails, throws an error.
|
|
126
|
+
* - If validation passes, deletes the document using the filtered query.
|
|
127
|
+
*/
|
|
128
|
+
deleteOne: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (query = {}) {
|
|
129
|
+
var _a;
|
|
130
|
+
if (!run_as_system) {
|
|
131
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.DELETE);
|
|
132
|
+
// Apply access control filters
|
|
133
|
+
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
134
|
+
// Retrieve the document to check permissions before deleting
|
|
135
|
+
const result = yield collection.findOne({ $and: formattedQuery });
|
|
136
|
+
const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
|
|
137
|
+
logDebug('delete winningRole', {
|
|
138
|
+
collection: collName,
|
|
139
|
+
userId: getUserId(user),
|
|
140
|
+
winningRoleName: (_a = winningRole === null || winningRole === void 0 ? void 0 : winningRole.name) !== null && _a !== void 0 ? _a : null
|
|
141
|
+
});
|
|
142
|
+
const { status } = winningRole
|
|
323
143
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
324
|
-
type: '
|
|
144
|
+
type: 'delete',
|
|
325
145
|
roles,
|
|
326
|
-
cursor:
|
|
146
|
+
cursor: result,
|
|
327
147
|
expansions: {}
|
|
328
148
|
}, user)
|
|
329
|
-
:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
...(projection ? [{ $project: projection }] : []),
|
|
362
|
-
...(0, utils_2.applyAccessControlToPipeline)(pipeline, rules, user)
|
|
363
|
-
];
|
|
364
|
-
// const pipelineCollections = getCollectionsFromPipeline(pipeline)
|
|
365
|
-
// console.log(pipelineCollections)
|
|
366
|
-
// pipelineCollections.every((collection) => checkDenyOperation(rules, collection, CRUD_OPERATIONS.READ))
|
|
367
|
-
const originalCursor = collection.aggregate(guardedPipeline, options);
|
|
368
|
-
const newCursor = Object.create(originalCursor);
|
|
369
|
-
newCursor.toArray = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
370
|
-
const results = yield originalCursor.toArray();
|
|
371
|
-
const filtered = yield Promise.all(results.map((doc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
372
|
-
const role = (0, utils_1.getWinningRole)(doc, user, roles);
|
|
373
|
-
const { status, document } = role
|
|
374
|
-
? yield (0, machines_1.checkValidation)(role, { type: 'read', roles, cursor: doc, expansions: {} }, user)
|
|
375
|
-
: { status: !(roles === null || roles === void 0 ? void 0 : roles.length), document: doc };
|
|
376
|
-
return status ? document : undefined;
|
|
377
|
-
})));
|
|
378
|
-
return filtered.filter(Boolean);
|
|
379
|
-
});
|
|
380
|
-
return newCursor;
|
|
381
|
-
}),
|
|
382
|
-
/**
|
|
383
|
-
* Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
|
|
384
|
-
*
|
|
385
|
-
* @param {OptionalId<Document>[]} documents - The array of documents to insert.
|
|
386
|
-
* @param {BulkWriteOptions} [options] - Optional settings passed to `insertMany`, such as `ordered`, `writeConcern`, etc.
|
|
387
|
-
* @returns {Promise<InsertManyResult<Document>>} A promise resolving to the result of the insert operation.
|
|
388
|
-
*
|
|
389
|
-
* @throws {Error} If no documents pass validation or user is not permitted to insert.
|
|
390
|
-
*
|
|
391
|
-
* @description
|
|
392
|
-
* If `run_as_system` is enabled, this function directly inserts the documents without validation.
|
|
393
|
-
* Otherwise, for each document:
|
|
394
|
-
* - Finds the user's applicable role using `getWinningRole`.
|
|
395
|
-
* - Validates the insert operation through `checkValidation`.
|
|
396
|
-
* - Filters out any documents the user is not authorized to insert.
|
|
397
|
-
* Only documents passing validation will be inserted.
|
|
398
|
-
*/
|
|
399
|
-
insertMany: (documents, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
400
|
-
if (!run_as_system) {
|
|
401
|
-
(0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
|
|
402
|
-
const { roles } = rules[collName] || {};
|
|
403
|
-
// Validate each document against user's roles
|
|
404
|
-
const filteredItems = yield Promise.all(documents.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
405
|
-
const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
|
|
149
|
+
: fallbackAccess(result);
|
|
150
|
+
if (!status) {
|
|
151
|
+
throw new Error('Delete not permitted');
|
|
152
|
+
}
|
|
153
|
+
return collection.deleteOne({ $and: formattedQuery });
|
|
154
|
+
}
|
|
155
|
+
// System mode: bypass access control
|
|
156
|
+
return collection.deleteOne(query);
|
|
157
|
+
}),
|
|
158
|
+
/**
|
|
159
|
+
* Inserts a single document into a MongoDB collection with optional role-based validation.
|
|
160
|
+
*
|
|
161
|
+
* @param {OptionalId<Document>} data - The document to insert.
|
|
162
|
+
* @param {InsertOneOptions} [options] - Optional settings for the insert operation, such as `writeConcern`.
|
|
163
|
+
* @returns {Promise<InsertOneResult<Document>>} A promise resolving to the result of the insert operation.
|
|
164
|
+
*
|
|
165
|
+
* @throws {Error} If the user is not authorized to insert the document.
|
|
166
|
+
*
|
|
167
|
+
* @description
|
|
168
|
+
* If `run_as_system` is enabled, the document is inserted directly without any validation.
|
|
169
|
+
* Otherwise:
|
|
170
|
+
* - Determines the appropriate user role using `getWinningRole`.
|
|
171
|
+
* - Validates the insert operation using `checkValidation`.
|
|
172
|
+
* - If validation fails, an error is thrown.
|
|
173
|
+
* - If validation passes, the document is inserted.
|
|
174
|
+
*
|
|
175
|
+
* This ensures that only users with the correct permissions can insert data into the collection.
|
|
176
|
+
*/
|
|
177
|
+
insertOne: (data, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
+
if (!run_as_system) {
|
|
179
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
|
|
180
|
+
const winningRole = (0, utils_1.getWinningRole)(data, user, roles);
|
|
406
181
|
const { status, document } = winningRole
|
|
407
182
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
408
183
|
type: 'insert',
|
|
409
184
|
roles,
|
|
410
|
-
cursor:
|
|
185
|
+
cursor: data,
|
|
411
186
|
expansions: {}
|
|
412
187
|
}, user)
|
|
413
|
-
:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
updateMany: (query, data, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
426
|
-
if (!run_as_system) {
|
|
427
|
-
(0, utils_2.checkDenyOperation)(rules, collection.collectionName, model_1.CRUD_OPERATIONS.UPDATE);
|
|
428
|
-
const { filters, roles } = rules[collName] || {};
|
|
429
|
-
// Apply access control filters
|
|
430
|
-
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
431
|
-
// Retrieve the document to check permissions before updating
|
|
432
|
-
const result = yield collection.find({ $and: formattedQuery }).toArray();
|
|
433
|
-
if (!result) {
|
|
434
|
-
console.log('check1 In updateMany --> (!result)');
|
|
435
|
-
throw new Error('Update not permitted');
|
|
188
|
+
: fallbackAccess(data);
|
|
189
|
+
if (!status || !(0, isEqual_1.default)(data, document)) {
|
|
190
|
+
throw new Error('Insert not permitted');
|
|
191
|
+
}
|
|
192
|
+
logService('insertOne payload', { collName, data });
|
|
193
|
+
const insertResult = yield collection.insertOne(data, options);
|
|
194
|
+
logService('insertOne result', {
|
|
195
|
+
collName,
|
|
196
|
+
insertedId: insertResult.insertedId.toString(),
|
|
197
|
+
document: data
|
|
198
|
+
});
|
|
199
|
+
return insertResult;
|
|
436
200
|
}
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
201
|
+
// System mode: insert without validation
|
|
202
|
+
return collection.insertOne(data, options);
|
|
203
|
+
}),
|
|
204
|
+
/**
|
|
205
|
+
* Updates a single document in a MongoDB collection with optional role-based validation.
|
|
206
|
+
*
|
|
207
|
+
* @param {Filter<Document>} query - The MongoDB query used to match the document to update.
|
|
208
|
+
* @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
|
|
209
|
+
* @param {UpdateOptions} [options] - Optional settings for the update operation.
|
|
210
|
+
* @returns {Promise<UpdateResult>} A promise resolving to the result of the update operation.
|
|
211
|
+
*
|
|
212
|
+
* @throws {Error} If the user is not authorized to update the document.
|
|
213
|
+
*
|
|
214
|
+
* @description
|
|
215
|
+
* If `run_as_system` is enabled, the function directly updates the document using `collection.updateOne(query, data, options)`.
|
|
216
|
+
* Otherwise, it follows these steps:
|
|
217
|
+
* - Applies access control filters to the query using `getFormattedQuery`.
|
|
218
|
+
* - Retrieves the document using `findOne` to check if it exists and whether the user has permission to modify it.
|
|
219
|
+
* - Determines the user's role via `getWinningRole`.
|
|
220
|
+
* - Flattens update operators (`$set`, `$inc`, etc.) if present to extract the final modified fields.
|
|
221
|
+
* - Validates the update data using `checkValidation` to ensure compliance with role-based rules.
|
|
222
|
+
* - Ensures that no unauthorized modifications occur by comparing the validated document with the intended changes.
|
|
223
|
+
* - If validation fails, throws an error; otherwise, updates the document.
|
|
224
|
+
*/
|
|
225
|
+
updateOne: (query, data, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
|
+
if (!run_as_system) {
|
|
227
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.UPDATE);
|
|
228
|
+
// Apply access control filters
|
|
229
|
+
// Normalize _id
|
|
230
|
+
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
231
|
+
const safeQuery = Array.isArray(formattedQuery)
|
|
232
|
+
? (0, utils_2.normalizeQuery)(formattedQuery)
|
|
233
|
+
: formattedQuery;
|
|
234
|
+
const result = yield collection.findOne({ $and: safeQuery });
|
|
235
|
+
if (!result) {
|
|
236
|
+
throw new Error('Update not permitted');
|
|
237
|
+
}
|
|
238
|
+
const winningRole = (0, utils_1.getWinningRole)(result, user, roles);
|
|
239
|
+
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
240
|
+
const hasOperators = Object.keys(data).some((key) => key.startsWith('$'));
|
|
241
|
+
// Flatten the update object to extract the actual fields being modified
|
|
242
|
+
// const docToCheck = hasOperators
|
|
243
|
+
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
244
|
+
// : data
|
|
245
|
+
const pipeline = [
|
|
246
|
+
{
|
|
247
|
+
$match: { $and: safeQuery }
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
$limit: 1
|
|
251
|
+
},
|
|
252
|
+
...Object.entries(data).map(([key, value]) => ({ [key]: value }))
|
|
253
|
+
];
|
|
254
|
+
const [docToCheck] = hasOperators
|
|
255
|
+
? yield collection.aggregate(pipeline).toArray()
|
|
256
|
+
: [data];
|
|
257
|
+
// Validate update permissions
|
|
454
258
|
const { status, document } = winningRole
|
|
455
259
|
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
456
260
|
type: 'write',
|
|
457
261
|
roles,
|
|
458
|
-
cursor:
|
|
262
|
+
cursor: docToCheck,
|
|
459
263
|
expansions: {}
|
|
460
264
|
}, user)
|
|
461
|
-
:
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
throw new Error('Update not permitted');
|
|
265
|
+
: fallbackAccess(docToCheck);
|
|
266
|
+
// Ensure no unauthorized changes are made
|
|
267
|
+
const areDocumentsEqual = (0, isEqual_1.default)(document, docToCheck);
|
|
268
|
+
if (!status || !areDocumentsEqual) {
|
|
269
|
+
throw new Error('Update not permitted');
|
|
270
|
+
}
|
|
271
|
+
return collection.updateOne({ $and: safeQuery }, data, options);
|
|
469
272
|
}
|
|
470
|
-
return collection.
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
273
|
+
return collection.updateOne(query, data, options);
|
|
274
|
+
}),
|
|
275
|
+
/**
|
|
276
|
+
* Finds documents in a MongoDB collection with optional role-based access control and post-query validation.
|
|
277
|
+
*
|
|
278
|
+
* @param {Filter<Document>} query - The MongoDB query to filter documents.
|
|
279
|
+
* @returns {FindCursor} A customized `FindCursor` that includes additional access control logic in its `toArray()` method.
|
|
280
|
+
*
|
|
281
|
+
* @description
|
|
282
|
+
* If `run_as_system` is enabled, the function simply returns a regular MongoDB cursor (`collection.find(query)`).
|
|
283
|
+
* Otherwise:
|
|
284
|
+
* - Combines the user query with role-based filters via `getFormattedQuery`.
|
|
285
|
+
* - Executes the query using `collection.find` with a `$and` of all filters.
|
|
286
|
+
* - Returns a cloned `FindCursor` where `toArray()`:
|
|
287
|
+
* - Applies additional post-query validation using `checkValidation` for each document.
|
|
288
|
+
* - Filters out documents the current user is not authorized to read.
|
|
289
|
+
*
|
|
290
|
+
* This ensures that both pre-query filtering and post-query validation are applied consistently.
|
|
291
|
+
*/
|
|
292
|
+
find: (query) => {
|
|
293
|
+
if (!run_as_system) {
|
|
294
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
295
|
+
// Pre-query filtering based on access control rules
|
|
296
|
+
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
297
|
+
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {};
|
|
298
|
+
// aggiunto filter per evitare questo errore: $and argument's entries must be objects
|
|
299
|
+
const cursor = collection.find(currentQuery);
|
|
300
|
+
const originalToArray = cursor.toArray.bind(cursor);
|
|
301
|
+
/**
|
|
302
|
+
* Overridden `toArray` method that validates each document for read access.
|
|
303
|
+
*
|
|
304
|
+
* @returns {Promise<Document[]>} An array of documents the user is authorized to read.
|
|
305
|
+
*/
|
|
306
|
+
cursor.toArray = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
307
|
+
const response = yield originalToArray();
|
|
308
|
+
const filteredResponse = yield Promise.all(response.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
309
|
+
var _a;
|
|
310
|
+
const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
|
|
311
|
+
logDebug('find winningRole', {
|
|
312
|
+
collection: collName,
|
|
313
|
+
userId: getUserId(user),
|
|
314
|
+
winningRoleName: (_a = winningRole === null || winningRole === void 0 ? void 0 : winningRole.name) !== null && _a !== void 0 ? _a : null,
|
|
315
|
+
rolesLength: roles.length
|
|
316
|
+
});
|
|
317
|
+
const { status, document } = winningRole
|
|
318
|
+
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
319
|
+
type: 'read',
|
|
320
|
+
roles,
|
|
321
|
+
cursor: currentDoc,
|
|
322
|
+
expansions: {}
|
|
323
|
+
}, user)
|
|
324
|
+
: fallbackAccess(currentDoc);
|
|
325
|
+
return status ? document : undefined;
|
|
326
|
+
})));
|
|
327
|
+
return filteredResponse.filter(Boolean);
|
|
515
328
|
});
|
|
329
|
+
return cursor;
|
|
516
330
|
}
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
331
|
+
// System mode: return original unfiltered cursor
|
|
332
|
+
return collection.find(query);
|
|
333
|
+
},
|
|
334
|
+
/**
|
|
335
|
+
* Watches changes on a MongoDB collection with optional role-based filtering of change events.
|
|
336
|
+
*
|
|
337
|
+
* @param {Document[]} [pipeline=[]] - Optional aggregation pipeline stages to apply to the change stream.
|
|
338
|
+
* @param {ChangeStreamOptions} [options] - Optional settings for the change stream, such as `fullDocument`, `resumeAfter`, etc.
|
|
339
|
+
* @returns {ChangeStream} A MongoDB `ChangeStream` instance, optionally enhanced with access control.
|
|
340
|
+
*
|
|
341
|
+
* @description
|
|
342
|
+
* If `run_as_system` is enabled, this function simply returns `collection.watch(pipeline, options)`.
|
|
343
|
+
* Otherwise:
|
|
344
|
+
* - Applies access control filters via `getFormattedQuery`.
|
|
345
|
+
* - Prepends a `$match` stage to the pipeline to limit watched changes to authorized documents.
|
|
346
|
+
* - Overrides the `.on()` method of the returned `ChangeStream` to:
|
|
347
|
+
* - Validate the `fullDocument` and any `updatedFields` using `checkValidation`.
|
|
348
|
+
* - Filter out change events the user is not authorized to see.
|
|
349
|
+
* - Pass only validated and filtered events to the original listener.
|
|
350
|
+
*
|
|
351
|
+
* This allows fine-grained control over what change events a user can observe, based on roles and filters.
|
|
352
|
+
*/
|
|
353
|
+
watch: (pipeline = [], options) => {
|
|
354
|
+
if (!run_as_system) {
|
|
355
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
356
|
+
// Apply access filters to initial change stream pipeline
|
|
357
|
+
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, {}, user);
|
|
358
|
+
const firstStep = formattedQuery.length ? {
|
|
359
|
+
$match: {
|
|
360
|
+
$and: formattedQuery
|
|
361
|
+
}
|
|
362
|
+
} : undefined;
|
|
363
|
+
const formattedPipeline = [
|
|
364
|
+
firstStep,
|
|
365
|
+
...pipeline
|
|
366
|
+
].filter(Boolean);
|
|
367
|
+
const result = collection.watch(formattedPipeline, options);
|
|
368
|
+
const originalOn = result.on.bind(result);
|
|
369
|
+
/**
|
|
370
|
+
* Validates a change event against the user's roles.
|
|
371
|
+
*
|
|
372
|
+
* @param {Document} change - A change event from the ChangeStream.
|
|
373
|
+
* @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
|
|
374
|
+
*/
|
|
375
|
+
const isValidChange = (_a) => __awaiter(void 0, [_a], void 0, function* ({ fullDocument, updateDescription }) {
|
|
376
|
+
const winningRole = (0, utils_1.getWinningRole)(fullDocument, user, roles);
|
|
377
|
+
const { status, document } = winningRole
|
|
378
|
+
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
379
|
+
type: 'read',
|
|
380
|
+
roles,
|
|
381
|
+
cursor: fullDocument,
|
|
382
|
+
expansions: {}
|
|
383
|
+
}, user)
|
|
384
|
+
: fallbackAccess(fullDocument);
|
|
385
|
+
const { status: updatedFieldsStatus, document: updatedFields } = winningRole
|
|
386
|
+
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
387
|
+
type: 'read',
|
|
388
|
+
roles,
|
|
389
|
+
cursor: updateDescription === null || updateDescription === void 0 ? void 0 : updateDescription.updatedFields,
|
|
390
|
+
expansions: {}
|
|
391
|
+
}, user)
|
|
392
|
+
: fallbackAccess(updateDescription === null || updateDescription === void 0 ? void 0 : updateDescription.updatedFields);
|
|
393
|
+
return { status, document, updatedFieldsStatus, updatedFields };
|
|
394
|
+
});
|
|
395
|
+
// Override the .on() method to apply validation before emitting events
|
|
396
|
+
result.on = (eventType, listener) => {
|
|
397
|
+
return originalOn(eventType, (change) => __awaiter(void 0, void 0, void 0, function* () {
|
|
398
|
+
const { status, document, updatedFieldsStatus, updatedFields } = yield isValidChange(change);
|
|
399
|
+
if (!status)
|
|
400
|
+
return;
|
|
401
|
+
const filteredChange = Object.assign(Object.assign({}, change), { fullDocument: document, updateDescription: Object.assign(Object.assign({}, change.updateDescription), { updatedFields: updatedFieldsStatus ? updatedFields : {} }) });
|
|
402
|
+
listener(filteredChange);
|
|
403
|
+
}));
|
|
404
|
+
};
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
// System mode: no filtering applied
|
|
408
|
+
return collection.watch(pipeline, options);
|
|
409
|
+
},
|
|
410
|
+
//TODO -> add filter & rules in aggregate
|
|
411
|
+
aggregate: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (pipeline = [], options, isClient) {
|
|
412
|
+
if (run_as_system || !isClient) {
|
|
413
|
+
return collection.aggregate(pipeline, options);
|
|
414
|
+
}
|
|
415
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
416
|
+
const rulesConfig = collectionRules !== null && collectionRules !== void 0 ? collectionRules : { filters, roles };
|
|
417
|
+
(0, utils_2.ensureClientPipelineStages)(pipeline);
|
|
418
|
+
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, {}, user);
|
|
419
|
+
logDebug('aggregate formattedQuery', {
|
|
420
|
+
collection: collName,
|
|
421
|
+
formattedQuery,
|
|
422
|
+
pipeline
|
|
423
|
+
});
|
|
424
|
+
const projection = (0, utils_2.getFormattedProjection)(filters);
|
|
425
|
+
const hiddenFields = (0, utils_2.getHiddenFieldsFromRulesConfig)(rulesConfig);
|
|
426
|
+
const sanitizedPipeline = (0, utils_2.applyAccessControlToPipeline)(pipeline, normalizedRules, user, collName, { isClientPipeline: true });
|
|
427
|
+
logDebug('aggregate sanitizedPipeline', {
|
|
428
|
+
collection: collName,
|
|
429
|
+
sanitizedPipeline
|
|
430
|
+
});
|
|
431
|
+
const guardedPipeline = [
|
|
432
|
+
...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
|
|
433
|
+
...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
|
|
434
|
+
...(projection ? [{ $project: projection }] : []),
|
|
435
|
+
...sanitizedPipeline
|
|
436
|
+
];
|
|
437
|
+
const originalCursor = collection.aggregate(guardedPipeline, options);
|
|
438
|
+
const newCursor = Object.create(originalCursor);
|
|
439
|
+
newCursor.toArray = () => __awaiter(void 0, void 0, void 0, function* () { return originalCursor.toArray(); });
|
|
440
|
+
return newCursor;
|
|
441
|
+
}),
|
|
442
|
+
/**
|
|
443
|
+
* Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
|
|
444
|
+
*
|
|
445
|
+
* @param {OptionalId<Document>[]} documents - The array of documents to insert.
|
|
446
|
+
* @param {BulkWriteOptions} [options] - Optional settings passed to `insertMany`, such as `ordered`, `writeConcern`, etc.
|
|
447
|
+
* @returns {Promise<InsertManyResult<Document>>} A promise resolving to the result of the insert operation.
|
|
448
|
+
*
|
|
449
|
+
* @throws {Error} If no documents pass validation or user is not permitted to insert.
|
|
450
|
+
*
|
|
451
|
+
* @description
|
|
452
|
+
* If `run_as_system` is enabled, this function directly inserts the documents without validation.
|
|
453
|
+
* Otherwise, for each document:
|
|
454
|
+
* - Finds the user's applicable role using `getWinningRole`.
|
|
455
|
+
* - Validates the insert operation through `checkValidation`.
|
|
456
|
+
* - Filters out any documents the user is not authorized to insert.
|
|
457
|
+
* Only documents passing validation will be inserted.
|
|
458
|
+
*/
|
|
459
|
+
insertMany: (documents, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
460
|
+
if (!run_as_system) {
|
|
461
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.CREATE);
|
|
462
|
+
// Validate each document against user's roles
|
|
463
|
+
const filteredItems = yield Promise.all(documents.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
464
|
+
const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
|
|
465
|
+
const { status, document } = winningRole
|
|
466
|
+
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
467
|
+
type: 'insert',
|
|
468
|
+
roles,
|
|
469
|
+
cursor: currentDoc,
|
|
470
|
+
expansions: {}
|
|
471
|
+
}, user)
|
|
472
|
+
: fallbackAccess(currentDoc);
|
|
473
|
+
return status ? document : undefined;
|
|
474
|
+
})));
|
|
475
|
+
const canInsert = (0, isEqual_1.default)(filteredItems, documents);
|
|
476
|
+
if (!canInsert) {
|
|
477
|
+
throw new Error('Insert not permitted');
|
|
478
|
+
}
|
|
479
|
+
return collection.insertMany(documents, options);
|
|
480
|
+
}
|
|
481
|
+
// If system mode is active, insert all documents without validation
|
|
482
|
+
return collection.insertMany(documents, options);
|
|
483
|
+
}),
|
|
484
|
+
updateMany: (query, data, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
485
|
+
if (!run_as_system) {
|
|
486
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.UPDATE);
|
|
487
|
+
// Apply access control filters
|
|
488
|
+
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
489
|
+
// Retrieve the document to check permissions before updating
|
|
490
|
+
const result = yield collection.find({ $and: formattedQuery }).toArray();
|
|
491
|
+
if (!result) {
|
|
492
|
+
console.log('check1 In updateMany --> (!result)');
|
|
493
|
+
throw new Error('Update not permitted');
|
|
494
|
+
}
|
|
495
|
+
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
496
|
+
const hasOperators = Object.keys(data).some((key) => key.startsWith('$'));
|
|
497
|
+
// Flatten the update object to extract the actual fields being modified
|
|
498
|
+
// const docToCheck = hasOperators
|
|
499
|
+
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
500
|
+
// : data
|
|
501
|
+
const pipeline = [
|
|
502
|
+
{
|
|
503
|
+
$match: { $and: formattedQuery }
|
|
504
|
+
},
|
|
505
|
+
...Object.entries(data).map(([key, value]) => ({ [key]: value }))
|
|
506
|
+
];
|
|
507
|
+
const docsToCheck = hasOperators
|
|
508
|
+
? yield collection.aggregate(pipeline).toArray()
|
|
509
|
+
: result;
|
|
510
|
+
const filteredItems = yield Promise.all(docsToCheck.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
511
|
+
const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
|
|
512
|
+
const { status, document } = winningRole
|
|
513
|
+
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
514
|
+
type: 'write',
|
|
515
|
+
roles,
|
|
516
|
+
cursor: currentDoc,
|
|
517
|
+
expansions: {}
|
|
518
|
+
}, user)
|
|
519
|
+
: fallbackAccess(currentDoc);
|
|
520
|
+
return status ? document : undefined;
|
|
521
|
+
})));
|
|
522
|
+
// Ensure no unauthorized changes are made
|
|
523
|
+
const areDocumentsEqual = (0, isEqual_1.default)(docsToCheck, filteredItems);
|
|
524
|
+
if (!areDocumentsEqual) {
|
|
525
|
+
console.log('check1 In updateMany --> (!areDocumentsEqual)');
|
|
526
|
+
throw new Error('Update not permitted');
|
|
527
|
+
}
|
|
528
|
+
return collection.updateMany({ $and: formattedQuery }, data, options);
|
|
529
|
+
}
|
|
530
|
+
return collection.updateMany(query, data, options);
|
|
531
|
+
}),
|
|
532
|
+
/**
|
|
533
|
+
* Deletes multiple documents from a MongoDB collection with role-based access control and validation.
|
|
534
|
+
*
|
|
535
|
+
* @param query - The initial MongoDB query to filter documents to be deleted.
|
|
536
|
+
* @returns {Promise<{ acknowledged: boolean, deletedCount: number }>} A promise resolving to the deletion result.
|
|
537
|
+
*
|
|
538
|
+
* @description
|
|
539
|
+
* If `run_as_system` is enabled, this function directly deletes documents matching the given query.
|
|
540
|
+
* Otherwise, it:
|
|
541
|
+
* - Applies additional filters from access control rules.
|
|
542
|
+
* - Fetches matching documents.
|
|
543
|
+
* - Validates each document against user roles.
|
|
544
|
+
* - Deletes only the documents that the current user has permission to delete.
|
|
545
|
+
*/
|
|
546
|
+
deleteMany: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (query = {}) {
|
|
547
|
+
if (!run_as_system) {
|
|
548
|
+
(0, utils_2.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.DELETE);
|
|
549
|
+
// Apply access control filters
|
|
550
|
+
const formattedQuery = (0, utils_2.getFormattedQuery)(filters, query, user);
|
|
551
|
+
// Fetch documents matching the combined filters
|
|
552
|
+
const data = yield collection.find({ $and: formattedQuery }).toArray();
|
|
553
|
+
// Filter and validate each document based on user's roles
|
|
554
|
+
const filteredItems = yield Promise.all(data.map((currentDoc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
555
|
+
const winningRole = (0, utils_1.getWinningRole)(currentDoc, user, roles);
|
|
556
|
+
const { status, document } = winningRole
|
|
557
|
+
? yield (0, machines_1.checkValidation)(winningRole, {
|
|
558
|
+
type: 'delete',
|
|
559
|
+
roles,
|
|
560
|
+
cursor: currentDoc,
|
|
561
|
+
expansions: {}
|
|
562
|
+
}, user)
|
|
563
|
+
: fallbackAccess(currentDoc);
|
|
564
|
+
return status ? document : undefined;
|
|
565
|
+
})));
|
|
566
|
+
// Extract IDs of documents that passed validation
|
|
567
|
+
const elementsToDelete = filteredItems.filter(Boolean).map(({ _id }) => _id);
|
|
568
|
+
if (!elementsToDelete.length) {
|
|
569
|
+
return Promise.resolve({
|
|
570
|
+
acknowledged: true,
|
|
571
|
+
deletedCount: 0
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// Build final delete query with access control and ID filter
|
|
575
|
+
const deleteQuery = {
|
|
576
|
+
$and: [...formattedQuery, { _id: { $in: elementsToDelete } }]
|
|
577
|
+
};
|
|
578
|
+
return collection.deleteMany(deleteQuery);
|
|
579
|
+
}
|
|
580
|
+
// If running as system, bypass access control and delete directly
|
|
581
|
+
return collection.deleteMany(query);
|
|
582
|
+
})
|
|
583
|
+
};
|
|
584
|
+
};
|
|
527
585
|
const MongodbAtlas = (app, { rules, user, run_as_system } = {}) => ({
|
|
528
586
|
db: (dbName) => {
|
|
529
587
|
return {
|
|
530
588
|
collection: (collName) => {
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
.collection(collName);
|
|
589
|
+
const mongoClient = app.mongo.client;
|
|
590
|
+
const collection = mongoClient.db(dbName).collection(collName);
|
|
534
591
|
return getOperators(collection, {
|
|
535
592
|
rules,
|
|
536
593
|
collName,
|