@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.
- package/README.md +28 -3
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +57 -3
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/jwt.js +49 -3
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +19 -3
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +125 -71
- package/dist/auth/providers/local-userpass/dtos.d.ts +11 -2
- package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
- package/dist/auth/utils.d.ts +53 -14
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +46 -63
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +18 -5
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +32 -3
- package/dist/features/functions/dtos.d.ts +3 -0
- package/dist/features/functions/dtos.d.ts.map +1 -1
- package/dist/features/functions/interface.d.ts +3 -0
- package/dist/features/functions/interface.d.ts.map +1 -1
- package/dist/features/functions/utils.d.ts +2 -1
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +19 -7
- package/dist/features/rules/utils.d.ts.map +1 -1
- package/dist/features/rules/utils.js +11 -2
- package/dist/features/triggers/index.d.ts.map +1 -1
- package/dist/features/triggers/index.js +48 -7
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +118 -27
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -21
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +605 -478
- package/dist/services/mongodb-atlas/model.d.ts +2 -1
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- 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 +4 -1
- 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 +7 -6
- 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 +1 -1
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +176 -5
- package/dist/utils/context/interface.d.ts +1 -1
- package/dist/utils/context/interface.d.ts.map +1 -1
- package/dist/utils/crypto/index.d.ts +1 -0
- package/dist/utils/crypto/index.d.ts.map +1 -1
- package/dist/utils/crypto/index.js +6 -2
- 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 +11 -3
- 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 -2
- package/src/auth/controller.ts +70 -4
- package/src/auth/plugins/jwt.test.ts +93 -0
- package/src/auth/plugins/jwt.ts +62 -3
- package/src/auth/providers/custom-function/controller.ts +22 -5
- package/src/auth/providers/local-userpass/controller.ts +168 -96
- package/src/auth/providers/local-userpass/dtos.ts +13 -2
- package/src/auth/utils.ts +51 -86
- package/src/constants.ts +17 -3
- package/src/fastify.d.ts +32 -15
- package/src/features/functions/controller.ts +51 -3
- package/src/features/functions/dtos.ts +3 -0
- package/src/features/functions/interface.ts +3 -0
- package/src/features/functions/utils.ts +29 -8
- package/src/features/rules/utils.ts +11 -2
- package/src/features/triggers/index.ts +43 -1
- package/src/features/triggers/utils.ts +146 -38
- package/src/index.ts +69 -20
- package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
- package/src/services/mongodb-atlas/index.ts +241 -90
- package/src/services/mongodb-atlas/model.ts +15 -2
- package/src/services/mongodb-atlas/utils.ts +158 -22
- package/src/shared/handleUserRegistration.ts +5 -4
- 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 +238 -13
- package/src/utils/context/interface.ts +1 -1
- package/src/utils/crypto/index.ts +5 -1
- package/src/utils/initializer/exposeRoutes.ts +15 -8
- package/src/utils/initializer/registerPlugins.ts +15 -7
- package/src/utils/roles/helpers.ts +23 -5
- 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
|
@@ -1,23 +1,62 @@
|
|
|
1
|
-
import { EventEmitterAsyncResourceOptions } from 'events'
|
|
2
1
|
import isEqual from 'lodash/isEqual'
|
|
3
|
-
import {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
:
|
|
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(
|
|
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
|
-
:
|
|
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(
|
|
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
|
-
:
|
|
216
|
+
: fallbackAccess(data)
|
|
155
217
|
|
|
156
218
|
if (!status || !isEqual(data, document)) {
|
|
157
219
|
throw new Error('Insert not permitted')
|
|
158
220
|
}
|
|
159
|
-
|
|
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(
|
|
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:
|
|
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
|
-
:
|
|
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:
|
|
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(
|
|
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
|
|
276
|
-
|
|
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
|
-
|
|
288
|
-
const response = await
|
|
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
|
-
:
|
|
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
|
|
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(
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
...
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
:
|
|
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
|
|
675
|
-
|
|
676
|
-
|
|
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 {
|
|
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
|
+
}
|