@flowerforce/flowerbase 1.2.0 → 1.2.1-beta.11

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.
Files changed (125) hide show
  1. package/README.md +28 -3
  2. package/dist/auth/controller.d.ts.map +1 -1
  3. package/dist/auth/controller.js +57 -3
  4. package/dist/auth/plugins/jwt.d.ts.map +1 -1
  5. package/dist/auth/plugins/jwt.js +49 -3
  6. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  7. package/dist/auth/providers/custom-function/controller.js +19 -3
  8. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  9. package/dist/auth/providers/local-userpass/controller.js +125 -71
  10. package/dist/auth/providers/local-userpass/dtos.d.ts +11 -2
  11. package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
  12. package/dist/auth/utils.d.ts +53 -14
  13. package/dist/auth/utils.d.ts.map +1 -1
  14. package/dist/auth/utils.js +46 -63
  15. package/dist/constants.d.ts +14 -0
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +18 -5
  18. package/dist/features/functions/controller.d.ts.map +1 -1
  19. package/dist/features/functions/controller.js +32 -3
  20. package/dist/features/functions/dtos.d.ts +3 -0
  21. package/dist/features/functions/dtos.d.ts.map +1 -1
  22. package/dist/features/functions/interface.d.ts +3 -0
  23. package/dist/features/functions/interface.d.ts.map +1 -1
  24. package/dist/features/functions/utils.d.ts +2 -1
  25. package/dist/features/functions/utils.d.ts.map +1 -1
  26. package/dist/features/functions/utils.js +19 -7
  27. package/dist/features/rules/utils.d.ts.map +1 -1
  28. package/dist/features/rules/utils.js +11 -2
  29. package/dist/features/triggers/index.d.ts.map +1 -1
  30. package/dist/features/triggers/index.js +48 -7
  31. package/dist/features/triggers/utils.d.ts.map +1 -1
  32. package/dist/features/triggers/utils.js +118 -27
  33. package/dist/index.d.ts +8 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +57 -21
  36. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  37. package/dist/services/mongodb-atlas/index.js +605 -478
  38. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  39. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  40. package/dist/services/mongodb-atlas/utils.d.ts +9 -2
  41. package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
  42. package/dist/services/mongodb-atlas/utils.js +113 -23
  43. package/dist/shared/handleUserRegistration.d.ts.map +1 -1
  44. package/dist/shared/handleUserRegistration.js +4 -1
  45. package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
  46. package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
  47. package/dist/utils/context/helpers.d.ts +7 -6
  48. package/dist/utils/context/helpers.d.ts.map +1 -1
  49. package/dist/utils/context/helpers.js +3 -0
  50. package/dist/utils/context/index.d.ts +1 -1
  51. package/dist/utils/context/index.d.ts.map +1 -1
  52. package/dist/utils/context/index.js +176 -5
  53. package/dist/utils/context/interface.d.ts +1 -1
  54. package/dist/utils/context/interface.d.ts.map +1 -1
  55. package/dist/utils/crypto/index.d.ts +1 -0
  56. package/dist/utils/crypto/index.d.ts.map +1 -1
  57. package/dist/utils/crypto/index.js +6 -2
  58. package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
  59. package/dist/utils/initializer/exposeRoutes.js +11 -4
  60. package/dist/utils/initializer/registerPlugins.d.ts +3 -1
  61. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  62. package/dist/utils/initializer/registerPlugins.js +9 -6
  63. package/dist/utils/roles/helpers.js +11 -3
  64. package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
  65. package/dist/utils/roles/machines/commonValidators.js +10 -6
  66. package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
  67. package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
  68. package/dist/utils/roles/machines/read/B/validators.js +8 -0
  69. package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
  70. package/dist/utils/roles/machines/read/C/index.js +10 -7
  71. package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
  72. package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
  73. package/dist/utils/roles/machines/read/C/validators.js +29 -0
  74. package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
  75. package/dist/utils/roles/machines/read/D/index.js +13 -11
  76. package/dist/utils/rules.d.ts +1 -1
  77. package/dist/utils/rules.d.ts.map +1 -1
  78. package/dist/utils/rules.js +26 -17
  79. package/jest.config.ts +2 -12
  80. package/jest.setup.ts +28 -0
  81. package/package.json +1 -2
  82. package/src/auth/controller.ts +70 -4
  83. package/src/auth/plugins/jwt.test.ts +93 -0
  84. package/src/auth/plugins/jwt.ts +62 -3
  85. package/src/auth/providers/custom-function/controller.ts +22 -5
  86. package/src/auth/providers/local-userpass/controller.ts +168 -96
  87. package/src/auth/providers/local-userpass/dtos.ts +13 -2
  88. package/src/auth/utils.ts +51 -86
  89. package/src/constants.ts +17 -3
  90. package/src/fastify.d.ts +32 -15
  91. package/src/features/functions/controller.ts +51 -3
  92. package/src/features/functions/dtos.ts +3 -0
  93. package/src/features/functions/interface.ts +3 -0
  94. package/src/features/functions/utils.ts +29 -8
  95. package/src/features/rules/utils.ts +11 -2
  96. package/src/features/triggers/index.ts +43 -1
  97. package/src/features/triggers/utils.ts +146 -38
  98. package/src/index.ts +69 -20
  99. package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
  100. package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
  101. package/src/services/mongodb-atlas/index.ts +241 -90
  102. package/src/services/mongodb-atlas/model.ts +15 -2
  103. package/src/services/mongodb-atlas/utils.ts +158 -22
  104. package/src/shared/handleUserRegistration.ts +5 -4
  105. package/src/shared/models/handleUserRegistration.model.ts +8 -3
  106. package/src/types/fastify-raw-body.d.ts +22 -0
  107. package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
  108. package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
  109. package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
  110. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
  111. package/src/utils/__tests__/registerPlugins.test.ts +16 -1
  112. package/src/utils/context/helpers.ts +3 -0
  113. package/src/utils/context/index.ts +238 -13
  114. package/src/utils/context/interface.ts +1 -1
  115. package/src/utils/crypto/index.ts +5 -1
  116. package/src/utils/initializer/exposeRoutes.ts +15 -8
  117. package/src/utils/initializer/registerPlugins.ts +15 -7
  118. package/src/utils/roles/helpers.ts +23 -5
  119. package/src/utils/roles/machines/commonValidators.ts +10 -5
  120. package/src/utils/roles/machines/read/B/validators.ts +8 -0
  121. package/src/utils/roles/machines/read/C/index.ts +11 -7
  122. package/src/utils/roles/machines/read/C/validators.ts +21 -0
  123. package/src/utils/roles/machines/read/D/index.ts +22 -12
  124. package/src/utils/rules.ts +31 -22
  125. package/tsconfig.spec.json +7 -0
@@ -1,23 +1,62 @@
1
- import { EventEmitterAsyncResourceOptions } from 'events'
2
1
  import isEqual from 'lodash/isEqual'
3
- import { Collection, Document, EventsDescription, FindCursor, WithId } from 'mongodb'
2
+ import {
3
+ Collection,
4
+ Document,
5
+ EventsDescription,
6
+ Filter as MongoFilter,
7
+ FindOneAndUpdateOptions,
8
+ UpdateFilter,
9
+ WithId
10
+ } from 'mongodb'
4
11
  import { checkValidation } from '../../utils/roles/machines'
5
12
  import { getWinningRole } from '../../utils/roles/machines/utils'
6
13
  import { CRUD_OPERATIONS, GetOperatorsFunction, MongodbAtlasFunction } from './model'
7
14
  import {
8
15
  applyAccessControlToPipeline,
9
16
  checkDenyOperation,
17
+ ensureClientPipelineStages,
10
18
  getFormattedProjection,
11
19
  getFormattedQuery,
20
+ getHiddenFieldsFromRulesConfig,
12
21
  normalizeQuery
13
22
  } from './utils'
23
+ import { Rules } from '../../features/rules/interface'
14
24
 
15
25
  //TODO aggiungere no-sql inject security
26
+ const debugRules = process.env.DEBUG_RULES === 'true'
27
+ const debugServices = process.env.DEBUG_SERVICES === 'true'
28
+
29
+ const logDebug = (message: string, payload?: unknown) => {
30
+ if (!debugRules) return
31
+ const formatted = payload && typeof payload === 'object' ? JSON.stringify(payload) : payload
32
+ console.log(`[rules-debug] ${message}`, formatted ?? '')
33
+ }
34
+
35
+ const getUserId = (user?: unknown) => {
36
+ if (!user || typeof user !== 'object') return undefined
37
+ return (user as { id?: string }).id
38
+ }
39
+
40
+ const logService = (message: string, payload?: unknown) => {
41
+ if (!debugServices) return
42
+ console.log('[service-debug]', message, payload ?? '')
43
+ }
44
+
16
45
  const getOperators: GetOperatorsFunction = (
17
46
  collection,
18
- { rules = {}, collName, user, run_as_system }
19
- ) => ({
20
- /**
47
+ { rules, collName, user, run_as_system }
48
+ ) => {
49
+ const normalizedRules: Rules = rules ?? ({} as Rules)
50
+ const collectionRules = normalizedRules[collName]
51
+ const filters = collectionRules?.filters ?? []
52
+ const roles = collectionRules?.roles ?? []
53
+ const fallbackAccess = (doc: Document | null | undefined = undefined) => ({
54
+ status: false,
55
+ document: doc
56
+ })
57
+
58
+ return {
59
+ /**
21
60
  * Finds a single document in a MongoDB collection with optional role-based filtering and validation.
22
61
  *
23
62
  * @param {Filter<Document>} query - The MongoDB query used to match the document.
@@ -34,16 +73,38 @@ const getOperators: GetOperatorsFunction = (
34
73
  */
35
74
  findOne: async (query) => {
36
75
  if (!run_as_system) {
37
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
38
- const { filters, roles } = rules[collName] || {}
39
-
76
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
40
77
  // Apply access control filters to the query
41
78
  const formattedQuery = getFormattedQuery(filters, query, user)
42
-
43
- const result = await collection.findOne({ $and: formattedQuery })
79
+ logDebug('update formattedQuery', {
80
+ collection: collName,
81
+ query,
82
+ formattedQuery
83
+ })
84
+ logDebug('find formattedQuery', {
85
+ collection: collName,
86
+ query,
87
+ formattedQuery,
88
+ rolesLength: roles.length
89
+ })
90
+
91
+ logService('findOne query', { collName, formattedQuery })
92
+ const safeQuery = normalizeQuery(formattedQuery)
93
+ logService('findOne normalizedQuery', { collName, safeQuery })
94
+ const result = await collection.findOne({ $and: safeQuery })
95
+ logDebug('findOne result', {
96
+ collection: collName,
97
+ result
98
+ })
99
+ logService('findOne result', { collName, result })
44
100
 
45
101
  const winningRole = getWinningRole(result, user, roles)
46
102
 
103
+ logDebug('findOne winningRole', {
104
+ collection: collName,
105
+ winningRoleName: winningRole?.name ?? null,
106
+ userId: getUserId(user)
107
+ })
47
108
  const { status, document } = winningRole
48
109
  ? await checkValidation(
49
110
  winningRole,
@@ -55,7 +116,7 @@ const getOperators: GetOperatorsFunction = (
55
116
  },
56
117
  user
57
118
  )
58
- : { status: true, document: result }
119
+ : fallbackAccess(result)
59
120
 
60
121
  // Return validated document or empty object if not permitted
61
122
  return Promise.resolve(status ? document : {})
@@ -82,9 +143,7 @@ const getOperators: GetOperatorsFunction = (
82
143
  */
83
144
  deleteOne: async (query = {}) => {
84
145
  if (!run_as_system) {
85
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.DELETE)
86
- const { filters, roles } = rules[collName] || {}
87
-
146
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
88
147
  // Apply access control filters
89
148
  const formattedQuery = getFormattedQuery(filters, query, user)
90
149
 
@@ -92,6 +151,11 @@ const getOperators: GetOperatorsFunction = (
92
151
  const result = await collection.findOne({ $and: formattedQuery })
93
152
  const winningRole = getWinningRole(result, user, roles)
94
153
 
154
+ logDebug('delete winningRole', {
155
+ collection: collName,
156
+ userId: getUserId(user),
157
+ winningRoleName: winningRole?.name ?? null
158
+ })
95
159
  const { status } = winningRole
96
160
  ? await checkValidation(
97
161
  winningRole,
@@ -103,7 +167,7 @@ const getOperators: GetOperatorsFunction = (
103
167
  },
104
168
  user
105
169
  )
106
- : { status: true }
170
+ : fallbackAccess(result)
107
171
 
108
172
  if (!status) {
109
173
  throw new Error('Delete not permitted')
@@ -134,10 +198,8 @@ const getOperators: GetOperatorsFunction = (
134
198
  * This ensures that only users with the correct permissions can insert data into the collection.
135
199
  */
136
200
  insertOne: async (data, options) => {
137
- const { roles } = rules[collName] || {}
138
-
139
201
  if (!run_as_system) {
140
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.CREATE)
202
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
141
203
  const winningRole = getWinningRole(data, user, roles)
142
204
 
143
205
  const { status, document } = winningRole
@@ -151,12 +213,19 @@ const getOperators: GetOperatorsFunction = (
151
213
  },
152
214
  user
153
215
  )
154
- : { status: true, document: data }
216
+ : fallbackAccess(data)
155
217
 
156
218
  if (!status || !isEqual(data, document)) {
157
219
  throw new Error('Insert not permitted')
158
220
  }
159
- return collection.insertOne(data, options)
221
+ logService('insertOne payload', { collName, data })
222
+ const insertResult = await collection.insertOne(data, options)
223
+ logService('insertOne result', {
224
+ collName,
225
+ insertedId: insertResult.insertedId.toString(),
226
+ document: data
227
+ })
228
+ return insertResult
160
229
  }
161
230
  // System mode: insert without validation
162
231
  return collection.insertOne(data, options)
@@ -185,8 +254,7 @@ const getOperators: GetOperatorsFunction = (
185
254
  updateOne: async (query, data, options) => {
186
255
  if (!run_as_system) {
187
256
 
188
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
189
- const { filters, roles } = rules[collName] || {}
257
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
190
258
  // Apply access control filters
191
259
 
192
260
  // Normalize _id
@@ -210,10 +278,9 @@ const getOperators: GetOperatorsFunction = (
210
278
  // const docToCheck = hasOperators
211
279
  // ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
212
280
  // : data
213
- const [matchQuery] = formattedQuery; // TODO da chiedere/capire perchè è solo uno. tutti gli altri { $match: { $and: formattedQuery } }
214
281
  const pipeline = [
215
282
  {
216
- $match: matchQuery
283
+ $match: { $and: safeQuery }
217
284
  },
218
285
  {
219
286
  $limit: 1
@@ -235,17 +302,107 @@ const getOperators: GetOperatorsFunction = (
235
302
  },
236
303
  user
237
304
  )
238
- : { status: true, document: docToCheck }
305
+ : fallbackAccess(docToCheck)
239
306
  // Ensure no unauthorized changes are made
240
307
  const areDocumentsEqual = isEqual(document, docToCheck)
241
308
 
242
309
  if (!status || !areDocumentsEqual) {
243
310
  throw new Error('Update not permitted')
244
311
  }
245
- return collection.updateOne({ $and: formattedQuery }, data, options)
312
+ return collection.updateOne({ $and: safeQuery }, data, options)
246
313
  }
247
314
  return collection.updateOne(query, data, options)
248
315
  },
316
+ /**
317
+ * Finds and updates a single document with role-based validation and access control.
318
+ *
319
+ * @param {Filter<Document>} query - The MongoDB query used to match the document to update.
320
+ * @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
321
+ * @param {FindOneAndUpdateOptions} [options] - Optional settings for the findOneAndUpdate operation.
322
+ * @returns {Promise<FindAndModifyResult<Document>>} The result of the findOneAndUpdate operation.
323
+ *
324
+ * @throws {Error} If the user is not authorized to update the document.
325
+ */
326
+ findOneAndUpdate: async (
327
+ query: MongoFilter<Document>,
328
+ data: UpdateFilter<Document> | Document[],
329
+ options?: FindOneAndUpdateOptions
330
+ ) => {
331
+ if (!run_as_system) {
332
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
333
+ const formattedQuery = getFormattedQuery(filters, query, user)
334
+ const safeQuery = Array.isArray(formattedQuery)
335
+ ? normalizeQuery(formattedQuery)
336
+ : formattedQuery
337
+
338
+ const result = await collection.findOne({ $and: safeQuery })
339
+
340
+ if (!result) {
341
+ throw new Error('Update not permitted')
342
+ }
343
+
344
+ const winningRole = getWinningRole(result, user, roles)
345
+ const hasOperators = Object.keys(data).some((key) => key.startsWith('$'))
346
+ const pipeline = [
347
+ {
348
+ $match: { $and: safeQuery }
349
+ },
350
+ {
351
+ $limit: 1
352
+ },
353
+ ...Object.entries(data).map(([key, value]) => ({ [key]: value }))
354
+ ]
355
+ const [docToCheck] = hasOperators
356
+ ? await collection.aggregate(pipeline).toArray()
357
+ : ([data] as [Document])
358
+
359
+ const { status, document } = winningRole
360
+ ? await checkValidation(
361
+ winningRole,
362
+ {
363
+ type: 'write',
364
+ roles,
365
+ cursor: docToCheck,
366
+ expansions: {}
367
+ },
368
+ user
369
+ )
370
+ : fallbackAccess(docToCheck)
371
+
372
+ const areDocumentsEqual = isEqual(document, docToCheck)
373
+ if (!status || !areDocumentsEqual) {
374
+ throw new Error('Update not permitted')
375
+ }
376
+
377
+ const updateResult = options
378
+ ? await collection.findOneAndUpdate({ $and: safeQuery }, data, options)
379
+ : await collection.findOneAndUpdate({ $and: safeQuery }, data)
380
+ if (!updateResult) {
381
+ return updateResult
382
+ }
383
+
384
+ const readRole = getWinningRole(updateResult, user, roles)
385
+ const readResult = readRole
386
+ ? await checkValidation(
387
+ readRole,
388
+ {
389
+ type: 'read',
390
+ roles,
391
+ cursor: updateResult,
392
+ expansions: {}
393
+ },
394
+ user
395
+ )
396
+ : fallbackAccess(updateResult)
397
+
398
+ const sanitizedDoc = readResult.status ? (readResult.document ?? updateResult) : {}
399
+ return sanitizedDoc
400
+ }
401
+
402
+ return options
403
+ ? collection.findOneAndUpdate(query, data, options)
404
+ : collection.findOneAndUpdate(query, data)
405
+ },
249
406
  /**
250
407
  * Finds documents in a MongoDB collection with optional role-based access control and post-query validation.
251
408
  *
@@ -265,32 +422,32 @@ const getOperators: GetOperatorsFunction = (
265
422
  */
266
423
  find: (query) => {
267
424
  if (!run_as_system) {
268
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
269
- const { filters, roles } = rules[collName] || {}
270
-
425
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
271
426
  // Pre-query filtering based on access control rules
272
427
  const formattedQuery = getFormattedQuery(filters, query, user)
273
428
  const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
274
429
  // aggiunto filter per evitare questo errore: $and argument's entries must be objects
275
- const originalCursor = collection.find(currentQuery)
276
- // Clone the cursor to override `toArray` with post-query validation
277
- const client = originalCursor[
278
- 'client' as keyof typeof originalCursor
279
- ] as EventEmitterAsyncResourceOptions
280
- const newCursor = new FindCursor(client)
430
+ const cursor = collection.find(currentQuery)
431
+ const originalToArray = cursor.toArray.bind(cursor)
281
432
 
282
433
  /**
283
434
  * Overridden `toArray` method that validates each document for read access.
284
435
  *
285
436
  * @returns {Promise<Document[]>} An array of documents the user is authorized to read.
286
437
  */
287
- newCursor.toArray = async () => {
288
- const response = await originalCursor.toArray()
438
+ cursor.toArray = async () => {
439
+ const response = await originalToArray()
289
440
 
290
441
  const filteredResponse = await Promise.all(
291
442
  response.map(async (currentDoc) => {
292
443
  const winningRole = getWinningRole(currentDoc, user, roles)
293
444
 
445
+ logDebug('find winningRole', {
446
+ collection: collName,
447
+ userId: getUserId(user),
448
+ winningRoleName: winningRole?.name ?? null,
449
+ rolesLength: roles.length
450
+ })
294
451
  const { status, document } = winningRole
295
452
  ? await checkValidation(
296
453
  winningRole,
@@ -302,16 +459,16 @@ const getOperators: GetOperatorsFunction = (
302
459
  },
303
460
  user
304
461
  )
305
- : { status: !roles.length, document: currentDoc }
462
+ : fallbackAccess(currentDoc)
306
463
 
307
464
  return status ? document : undefined
308
465
  })
309
466
  )
310
467
 
311
- return filteredResponse.filter(Boolean)
468
+ return filteredResponse.filter(Boolean) as WithId<Document>[]
312
469
  }
313
470
 
314
- return newCursor
471
+ return cursor
315
472
  }
316
473
  // System mode: return original unfiltered cursor
317
474
  return collection.find(query)
@@ -337,9 +494,7 @@ const getOperators: GetOperatorsFunction = (
337
494
  */
338
495
  watch: (pipeline = [], options) => {
339
496
  if (!run_as_system) {
340
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
341
- const { filters, roles } = rules[collName] || {}
342
-
497
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
343
498
  // Apply access filters to initial change stream pipeline
344
499
  const formattedQuery = getFormattedQuery(filters, {}, user)
345
500
 
@@ -377,7 +532,7 @@ const getOperators: GetOperatorsFunction = (
377
532
  },
378
533
  user
379
534
  )
380
- : { status: true, document: fullDocument }
535
+ : fallbackAccess(fullDocument)
381
536
 
382
537
  const { status: updatedFieldsStatus, document: updatedFields } = winningRole
383
538
  ? await checkValidation(
@@ -390,7 +545,7 @@ const getOperators: GetOperatorsFunction = (
390
545
  },
391
546
  user
392
547
  )
393
- : { status: true, document: updateDescription?.updatedFields }
548
+ : fallbackAccess(updateDescription?.updatedFields)
394
549
 
395
550
  return { status, document, updatedFieldsStatus, updatedFields }
396
551
  }
@@ -425,52 +580,48 @@ const getOperators: GetOperatorsFunction = (
425
580
  },
426
581
  //TODO -> add filter & rules in aggregate
427
582
  aggregate: async (pipeline = [], options, isClient) => {
428
- if (isClient) {
429
- throw new Error("Aggregate operator from cliente is not implemented! Move it to a function")
430
- }
431
583
  if (run_as_system || !isClient) {
432
584
  return collection.aggregate(pipeline, options)
433
585
  }
434
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
435
586
 
436
- const { filters = [], roles = [] } = rules[collection.collectionName] || {}
587
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
588
+
589
+ const rulesConfig = collectionRules ?? { filters, roles }
590
+
591
+ ensureClientPipelineStages(pipeline)
592
+
437
593
  const formattedQuery = getFormattedQuery(filters, {}, user)
594
+ logDebug('aggregate formattedQuery', {
595
+ collection: collName,
596
+ formattedQuery,
597
+ pipeline
598
+ })
438
599
  const projection = getFormattedProjection(filters)
600
+ const hiddenFields = getHiddenFieldsFromRulesConfig(rulesConfig)
601
+
602
+ const sanitizedPipeline = applyAccessControlToPipeline(
603
+ pipeline,
604
+ normalizedRules,
605
+ user,
606
+ collName,
607
+ { isClientPipeline: true }
608
+ )
609
+ logDebug('aggregate sanitizedPipeline', {
610
+ collection: collName,
611
+ sanitizedPipeline
612
+ })
439
613
 
440
614
  const guardedPipeline = [
615
+ ...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
441
616
  ...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
442
617
  ...(projection ? [{ $project: projection }] : []),
443
- ...applyAccessControlToPipeline(pipeline, rules, user)
618
+ ...sanitizedPipeline
444
619
  ]
445
620
 
446
- // const pipelineCollections = getCollectionsFromPipeline(pipeline)
447
-
448
- // console.log(pipelineCollections)
449
-
450
- // pipelineCollections.every((collection) => checkDenyOperation(rules, collection, CRUD_OPERATIONS.READ))
451
-
452
621
  const originalCursor = collection.aggregate(guardedPipeline, options)
453
622
  const newCursor = Object.create(originalCursor)
454
623
 
455
- newCursor.toArray = async () => {
456
- const results = await originalCursor.toArray()
457
-
458
- const filtered = await Promise.all(
459
- results.map(async (doc) => {
460
- const role = getWinningRole(doc, user, roles)
461
- const { status, document } = role
462
- ? await checkValidation(
463
- role,
464
- { type: 'read', roles, cursor: doc, expansions: {} },
465
- user
466
- )
467
- : { status: !roles?.length, document: doc }
468
- return status ? document : undefined
469
- })
470
- )
471
-
472
- return filtered.filter(Boolean)
473
- }
624
+ newCursor.toArray = async () => originalCursor.toArray()
474
625
 
475
626
  return newCursor
476
627
  },
@@ -493,8 +644,7 @@ const getOperators: GetOperatorsFunction = (
493
644
  */
494
645
  insertMany: async (documents, options) => {
495
646
  if (!run_as_system) {
496
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.CREATE)
497
- const { roles } = rules[collName] || {}
647
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
498
648
  // Validate each document against user's roles
499
649
  const filteredItems = await Promise.all(
500
650
  documents.map(async (currentDoc) => {
@@ -511,7 +661,7 @@ const getOperators: GetOperatorsFunction = (
511
661
  },
512
662
  user
513
663
  )
514
- : { status: !roles.length, document: currentDoc }
664
+ : fallbackAccess(currentDoc)
515
665
 
516
666
  return status ? document : undefined
517
667
  })
@@ -530,8 +680,7 @@ const getOperators: GetOperatorsFunction = (
530
680
  },
531
681
  updateMany: async (query, data, options) => {
532
682
  if (!run_as_system) {
533
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
534
- const { filters, roles } = rules[collName] || {}
683
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
535
684
  // Apply access control filters
536
685
  const formattedQuery = getFormattedQuery(filters, query, user)
537
686
 
@@ -576,7 +725,7 @@ const getOperators: GetOperatorsFunction = (
576
725
  },
577
726
  user
578
727
  )
579
- : { status: !roles.length, document: currentDoc }
728
+ : fallbackAccess(currentDoc)
580
729
 
581
730
  return status ? document : undefined
582
731
  })
@@ -611,9 +760,7 @@ const getOperators: GetOperatorsFunction = (
611
760
  */
612
761
  deleteMany: async (query = {}) => {
613
762
  if (!run_as_system) {
614
- checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.DELETE)
615
- const { filters, roles } = rules[collName] || {}
616
-
763
+ checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
617
764
  // Apply access control filters
618
765
  const formattedQuery = getFormattedQuery(filters, query, user)
619
766
 
@@ -636,7 +783,7 @@ const getOperators: GetOperatorsFunction = (
636
783
  },
637
784
  user
638
785
  )
639
- : { status: !roles.length, document: currentDoc }
786
+ : fallbackAccess(currentDoc)
640
787
 
641
788
  return status ? document : undefined
642
789
  })
@@ -662,7 +809,8 @@ const getOperators: GetOperatorsFunction = (
662
809
  // If running as system, bypass access control and delete directly
663
810
  return collection.deleteMany(query)
664
811
  }
665
- })
812
+ }
813
+ }
666
814
 
667
815
  const MongodbAtlas: MongodbAtlasFunction = (
668
816
  app,
@@ -671,9 +819,12 @@ const MongodbAtlas: MongodbAtlasFunction = (
671
819
  db: (dbName: string) => {
672
820
  return {
673
821
  collection: (collName: string) => {
674
- const collection: Collection<Document> = app.mongo.client
675
- .db(dbName)
676
- .collection(collName)
822
+ const mongoClient = app.mongo.client as unknown as {
823
+ db: (database: string) => {
824
+ collection: (name: string) => Collection<Document>
825
+ }
826
+ }
827
+ const collection: Collection<Document> = mongoClient.db(dbName).collection(collName)
677
828
  return getOperators(collection, {
678
829
  rules,
679
830
  collName,
@@ -1,5 +1,13 @@
1
1
  import { FastifyInstance } from 'fastify'
2
- import { Collection, Document, FindCursor, WithId } from 'mongodb'
2
+ import {
3
+ Collection,
4
+ Document,
5
+ Filter as MongoFilter,
6
+ FindCursor,
7
+ FindOneAndUpdateOptions,
8
+ UpdateFilter,
9
+ WithId
10
+ } from 'mongodb'
3
11
  import { User } from '../../auth/dtos'
4
12
  import { Filter, Rules } from '../../features/rules/interface'
5
13
  import { Role } from '../../utils/roles/interface'
@@ -50,6 +58,11 @@ export type GetOperatorsFunction = (
50
58
  updateOne: (
51
59
  ...params: Parameters<Method<'updateOne'>>
52
60
  ) => ReturnType<Method<'updateOne'>>
61
+ findOneAndUpdate: (
62
+ filter: MongoFilter<Document>,
63
+ update: UpdateFilter<Document> | Document[],
64
+ options?: FindOneAndUpdateOptions
65
+ ) => Promise<Document | null>
53
66
  find: (...params: Parameters<Method<'find'>>) => FindCursor
54
67
  watch: (...params: Parameters<Method<'watch'>>) => ReturnType<Method<'watch'>>
55
68
  aggregate: (
@@ -73,4 +86,4 @@ export enum CRUD_OPERATIONS {
73
86
  UPDATE = "UPDATE",
74
87
  DELETE = "DELETE"
75
88
 
76
- }
89
+ }