@flowerforce/flowerbase 1.2.1-beta.2 → 1.2.1-beta.21
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 +37 -6
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +55 -4
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/jwt.js +52 -6
- package/dist/auth/providers/anon-user/controller.d.ts +8 -0
- package/dist/auth/providers/anon-user/controller.d.ts.map +1 -0
- package/dist/auth/providers/anon-user/controller.js +90 -0
- package/dist/auth/providers/anon-user/dtos.d.ts +10 -0
- package/dist/auth/providers/anon-user/dtos.d.ts.map +1 -0
- package/dist/auth/providers/anon-user/dtos.js +2 -0
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +35 -25
- package/dist/auth/providers/custom-function/dtos.d.ts +4 -1
- package/dist/auth/providers/custom-function/dtos.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +159 -73
- package/dist/auth/providers/local-userpass/dtos.d.ts +17 -2
- package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
- package/dist/auth/utils.d.ts +76 -14
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +55 -61
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -4
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +31 -12
- 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 +3 -2
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +19 -7
- package/dist/features/triggers/index.d.ts.map +1 -1
- package/dist/features/triggers/index.js +49 -7
- package/dist/features/triggers/interface.d.ts +1 -0
- package/dist/features/triggers/interface.d.ts.map +1 -1
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +67 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -13
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +72 -2
- package/dist/services/mongodb-atlas/model.d.ts +3 -2
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +3 -1
- package/dist/shared/handleUserRegistration.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.js +66 -1
- package/dist/shared/models/handleUserRegistration.model.d.ts +2 -1
- package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
- package/dist/shared/models/handleUserRegistration.model.js +1 -0
- package/dist/utils/context/helpers.d.ts +6 -6
- package/dist/utils/context/helpers.d.ts.map +1 -1
- 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 -9
- 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.js +1 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +12 -4
- package/dist/utils/roles/helpers.js +2 -1
- package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
- package/dist/utils/rules-matcher/utils.js +3 -0
- package/package.json +1 -2
- package/src/auth/controller.ts +71 -5
- package/src/auth/plugins/jwt.test.ts +93 -0
- package/src/auth/plugins/jwt.ts +67 -8
- package/src/auth/providers/anon-user/controller.ts +91 -0
- package/src/auth/providers/anon-user/dtos.ts +10 -0
- package/src/auth/providers/custom-function/controller.ts +40 -31
- package/src/auth/providers/custom-function/dtos.ts +5 -1
- package/src/auth/providers/local-userpass/controller.ts +211 -101
- package/src/auth/providers/local-userpass/dtos.ts +20 -2
- package/src/auth/utils.ts +66 -83
- package/src/constants.ts +14 -2
- package/src/features/functions/controller.ts +42 -12
- 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/triggers/index.ts +44 -1
- package/src/features/triggers/interface.ts +1 -0
- package/src/features/triggers/utils.ts +89 -37
- package/src/index.ts +49 -13
- package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
- package/src/services/mongodb-atlas/index.ts +665 -567
- package/src/services/mongodb-atlas/model.ts +16 -3
- package/src/services/mongodb-atlas/utils.ts +3 -0
- package/src/shared/handleUserRegistration.ts +83 -2
- package/src/shared/models/handleUserRegistration.model.ts +2 -1
- package/src/utils/__tests__/registerPlugins.test.ts +5 -1
- package/src/utils/context/index.ts +238 -18
- package/src/utils/context/interface.ts +1 -1
- package/src/utils/crypto/index.ts +5 -1
- package/src/utils/initializer/exposeRoutes.ts +1 -1
- package/src/utils/initializer/registerPlugins.ts +8 -0
- package/src/utils/roles/helpers.ts +3 -2
- package/src/utils/rules-matcher/utils.ts +3 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import isEqual from 'lodash/isEqual'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Collection,
|
|
4
|
+
Document,
|
|
5
|
+
EventsDescription,
|
|
6
|
+
FindOneAndUpdateOptions,
|
|
7
|
+
Filter as MongoFilter,
|
|
8
|
+
UpdateFilter,
|
|
9
|
+
WithId
|
|
10
|
+
} from 'mongodb'
|
|
11
|
+
import { Rules } from '../../features/rules/interface'
|
|
3
12
|
import { checkValidation } from '../../utils/roles/machines'
|
|
4
13
|
import { getWinningRole } from '../../utils/roles/machines/utils'
|
|
5
14
|
import { CRUD_OPERATIONS, GetOperatorsFunction, MongodbAtlasFunction } from './model'
|
|
@@ -12,7 +21,6 @@ import {
|
|
|
12
21
|
getHiddenFieldsFromRulesConfig,
|
|
13
22
|
normalizeQuery
|
|
14
23
|
} from './utils'
|
|
15
|
-
import { Rules } from '../../features/rules/interface'
|
|
16
24
|
|
|
17
25
|
//TODO aggiungere no-sql inject security
|
|
18
26
|
const debugRules = process.env.DEBUG_RULES === 'true'
|
|
@@ -63,654 +71,744 @@ const getOperators: GetOperatorsFunction = (
|
|
|
63
71
|
* - Validates the result using `checkValidation` to ensure read permission.
|
|
64
72
|
* - If validation fails, returns an empty object; otherwise returns the validated document.
|
|
65
73
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
logService('findOne query', { collName, formattedQuery })
|
|
84
|
-
const safeQuery = normalizeQuery(formattedQuery)
|
|
85
|
-
logService('findOne normalizedQuery', { collName, safeQuery })
|
|
86
|
-
const result = await collection.findOne({ $and: safeQuery })
|
|
87
|
-
logDebug('findOne result', {
|
|
88
|
-
collection: collName,
|
|
89
|
-
result
|
|
90
|
-
})
|
|
91
|
-
logService('findOne result', { collName, result })
|
|
92
|
-
|
|
93
|
-
const winningRole = getWinningRole(result, user, roles)
|
|
94
|
-
|
|
95
|
-
logDebug('findOne winningRole', {
|
|
96
|
-
collection: collName,
|
|
97
|
-
winningRoleName: winningRole?.name ?? null,
|
|
98
|
-
userId: getUserId(user)
|
|
99
|
-
})
|
|
100
|
-
const { status, document } = winningRole
|
|
101
|
-
? await checkValidation(
|
|
102
|
-
winningRole,
|
|
103
|
-
{
|
|
104
|
-
type: 'read',
|
|
105
|
-
roles,
|
|
106
|
-
cursor: result,
|
|
107
|
-
expansions: {}
|
|
108
|
-
},
|
|
109
|
-
user
|
|
110
|
-
)
|
|
111
|
-
: fallbackAccess(result)
|
|
74
|
+
findOne: async (query) => {
|
|
75
|
+
if (!run_as_system) {
|
|
76
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
77
|
+
// Apply access control filters to the query
|
|
78
|
+
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
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
|
+
})
|
|
112
90
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* @param {Filter<Document>} [query={}] - The MongoDB query used to match the document to delete.
|
|
123
|
-
* @returns {Promise<DeleteResult>} A promise resolving to the result of the delete operation.
|
|
124
|
-
*
|
|
125
|
-
* @throws {Error} If the user is not authorized to delete the document.
|
|
126
|
-
*
|
|
127
|
-
* @description
|
|
128
|
-
* If `run_as_system` is enabled, the function deletes the document directly using `collection.deleteOne(query)`.
|
|
129
|
-
* Otherwise:
|
|
130
|
-
* - Applies role-based and custom filters to the query using `getFormattedQuery`.
|
|
131
|
-
* - Retrieves the document using `findOne` to validate user permissions.
|
|
132
|
-
* - Checks if the user has the appropriate role to perform a delete via `checkValidation`.
|
|
133
|
-
* - If validation fails, throws an error.
|
|
134
|
-
* - If validation passes, deletes the document using the filtered query.
|
|
135
|
-
*/
|
|
136
|
-
deleteOne: async (query = {}) => {
|
|
137
|
-
if (!run_as_system) {
|
|
138
|
-
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
|
|
139
|
-
// Apply access control filters
|
|
140
|
-
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
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 })
|
|
141
100
|
|
|
142
|
-
|
|
143
|
-
const result = await collection.findOne({ $and: formattedQuery })
|
|
144
|
-
const winningRole = getWinningRole(result, user, roles)
|
|
101
|
+
const winningRole = getWinningRole(result, user, roles)
|
|
145
102
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
103
|
+
logDebug('findOne winningRole', {
|
|
104
|
+
collection: collName,
|
|
105
|
+
winningRoleName: winningRole?.name ?? null,
|
|
106
|
+
userId: getUserId(user)
|
|
107
|
+
})
|
|
108
|
+
const { status, document } = winningRole
|
|
109
|
+
? await checkValidation(
|
|
110
|
+
winningRole,
|
|
111
|
+
{
|
|
112
|
+
type: 'read',
|
|
113
|
+
roles,
|
|
114
|
+
cursor: result,
|
|
115
|
+
expansions: {}
|
|
116
|
+
},
|
|
117
|
+
user
|
|
118
|
+
)
|
|
119
|
+
: fallbackAccess(result)
|
|
163
120
|
|
|
164
|
-
|
|
165
|
-
|
|
121
|
+
// Return validated document or empty object if not permitted
|
|
122
|
+
return Promise.resolve(status ? document : {})
|
|
166
123
|
}
|
|
124
|
+
// System mode: no validation applied
|
|
125
|
+
return collection.findOne(query)
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* Deletes a single document from a MongoDB collection with optional role-based validation.
|
|
129
|
+
*
|
|
130
|
+
* @param {Filter<Document>} [query={}] - The MongoDB query used to match the document to delete.
|
|
131
|
+
* @returns {Promise<DeleteResult>} A promise resolving to the result of the delete operation.
|
|
132
|
+
*
|
|
133
|
+
* @throws {Error} If the user is not authorized to delete the document.
|
|
134
|
+
*
|
|
135
|
+
* @description
|
|
136
|
+
* If `run_as_system` is enabled, the function deletes the document directly using `collection.deleteOne(query)`.
|
|
137
|
+
* Otherwise:
|
|
138
|
+
* - Applies role-based and custom filters to the query using `getFormattedQuery`.
|
|
139
|
+
* - Retrieves the document using `findOne` to validate user permissions.
|
|
140
|
+
* - Checks if the user has the appropriate role to perform a delete via `checkValidation`.
|
|
141
|
+
* - If validation fails, throws an error.
|
|
142
|
+
* - If validation passes, deletes the document using the filtered query.
|
|
143
|
+
*/
|
|
144
|
+
deleteOne: async (query = {}) => {
|
|
145
|
+
if (!run_as_system) {
|
|
146
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
|
|
147
|
+
// Apply access control filters
|
|
148
|
+
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
149
|
+
|
|
150
|
+
// Retrieve the document to check permissions before deleting
|
|
151
|
+
const result = await collection.findOne({ $and: formattedQuery })
|
|
152
|
+
const winningRole = getWinningRole(result, user, roles)
|
|
153
|
+
|
|
154
|
+
logDebug('delete winningRole', {
|
|
155
|
+
collection: collName,
|
|
156
|
+
userId: getUserId(user),
|
|
157
|
+
winningRoleName: winningRole?.name ?? null
|
|
158
|
+
})
|
|
159
|
+
const { status } = winningRole
|
|
160
|
+
? await checkValidation(
|
|
161
|
+
winningRole,
|
|
162
|
+
{
|
|
163
|
+
type: 'delete',
|
|
164
|
+
roles,
|
|
165
|
+
cursor: result,
|
|
166
|
+
expansions: {}
|
|
167
|
+
},
|
|
168
|
+
user
|
|
169
|
+
)
|
|
170
|
+
: fallbackAccess(result)
|
|
167
171
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return collection.deleteOne(query)
|
|
172
|
-
},
|
|
173
|
-
/**
|
|
174
|
-
* Inserts a single document into a MongoDB collection with optional role-based validation.
|
|
175
|
-
*
|
|
176
|
-
* @param {OptionalId<Document>} data - The document to insert.
|
|
177
|
-
* @param {InsertOneOptions} [options] - Optional settings for the insert operation, such as `writeConcern`.
|
|
178
|
-
* @returns {Promise<InsertOneResult<Document>>} A promise resolving to the result of the insert operation.
|
|
179
|
-
*
|
|
180
|
-
* @throws {Error} If the user is not authorized to insert the document.
|
|
181
|
-
*
|
|
182
|
-
* @description
|
|
183
|
-
* If `run_as_system` is enabled, the document is inserted directly without any validation.
|
|
184
|
-
* Otherwise:
|
|
185
|
-
* - Determines the appropriate user role using `getWinningRole`.
|
|
186
|
-
* - Validates the insert operation using `checkValidation`.
|
|
187
|
-
* - If validation fails, an error is thrown.
|
|
188
|
-
* - If validation passes, the document is inserted.
|
|
189
|
-
*
|
|
190
|
-
* This ensures that only users with the correct permissions can insert data into the collection.
|
|
191
|
-
*/
|
|
192
|
-
insertOne: async (data, options) => {
|
|
193
|
-
if (!run_as_system) {
|
|
194
|
-
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
|
|
195
|
-
const winningRole = getWinningRole(data, user, roles)
|
|
196
|
-
|
|
197
|
-
const { status, document } = winningRole
|
|
198
|
-
? await checkValidation(
|
|
199
|
-
winningRole,
|
|
200
|
-
{
|
|
201
|
-
type: 'insert',
|
|
202
|
-
roles,
|
|
203
|
-
cursor: data,
|
|
204
|
-
expansions: {}
|
|
205
|
-
},
|
|
206
|
-
user
|
|
207
|
-
)
|
|
208
|
-
: fallbackAccess(data)
|
|
172
|
+
if (!status) {
|
|
173
|
+
throw new Error('Delete not permitted')
|
|
174
|
+
}
|
|
209
175
|
|
|
210
|
-
|
|
211
|
-
throw new Error('Insert not permitted')
|
|
176
|
+
return collection.deleteOne({ $and: formattedQuery })
|
|
212
177
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
* - Retrieves the document using `findOne` to check if it exists and whether the user has permission to modify it.
|
|
240
|
-
* - Determines the user's role via `getWinningRole`.
|
|
241
|
-
* - Flattens update operators (`$set`, `$inc`, etc.) if present to extract the final modified fields.
|
|
242
|
-
* - Validates the update data using `checkValidation` to ensure compliance with role-based rules.
|
|
243
|
-
* - Ensures that no unauthorized modifications occur by comparing the validated document with the intended changes.
|
|
244
|
-
* - If validation fails, throws an error; otherwise, updates the document.
|
|
245
|
-
*/
|
|
246
|
-
updateOne: async (query, data, options) => {
|
|
247
|
-
if (!run_as_system) {
|
|
178
|
+
// System mode: bypass access control
|
|
179
|
+
return collection.deleteOne(query)
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* Inserts a single document into a MongoDB collection with optional role-based validation.
|
|
183
|
+
*
|
|
184
|
+
* @param {OptionalId<Document>} data - The document to insert.
|
|
185
|
+
* @param {InsertOneOptions} [options] - Optional settings for the insert operation, such as `writeConcern`.
|
|
186
|
+
* @returns {Promise<InsertOneResult<Document>>} A promise resolving to the result of the insert operation.
|
|
187
|
+
*
|
|
188
|
+
* @throws {Error} If the user is not authorized to insert the document.
|
|
189
|
+
*
|
|
190
|
+
* @description
|
|
191
|
+
* If `run_as_system` is enabled, the document is inserted directly without any validation.
|
|
192
|
+
* Otherwise:
|
|
193
|
+
* - Determines the appropriate user role using `getWinningRole`.
|
|
194
|
+
* - Validates the insert operation using `checkValidation`.
|
|
195
|
+
* - If validation fails, an error is thrown.
|
|
196
|
+
* - If validation passes, the document is inserted.
|
|
197
|
+
*
|
|
198
|
+
* This ensures that only users with the correct permissions can insert data into the collection.
|
|
199
|
+
*/
|
|
200
|
+
insertOne: async (data, options) => {
|
|
201
|
+
if (!run_as_system) {
|
|
202
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
|
|
203
|
+
const winningRole = getWinningRole(data, user, roles)
|
|
248
204
|
|
|
249
|
-
|
|
250
|
-
|
|
205
|
+
const { status, document } = winningRole
|
|
206
|
+
? await checkValidation(
|
|
207
|
+
winningRole,
|
|
208
|
+
{
|
|
209
|
+
type: 'insert',
|
|
210
|
+
roles,
|
|
211
|
+
cursor: data,
|
|
212
|
+
expansions: {}
|
|
213
|
+
},
|
|
214
|
+
user
|
|
215
|
+
)
|
|
216
|
+
: fallbackAccess(data)
|
|
251
217
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
218
|
+
if (!status || !isEqual(data, document)) {
|
|
219
|
+
throw new Error('Insert not permitted')
|
|
220
|
+
}
|
|
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
|
|
229
|
+
}
|
|
230
|
+
// System mode: insert without validation
|
|
231
|
+
return collection.insertOne(data, options)
|
|
232
|
+
},
|
|
233
|
+
/**
|
|
234
|
+
* Updates a single document in a MongoDB collection with optional role-based validation.
|
|
235
|
+
*
|
|
236
|
+
* @param {Filter<Document>} query - The MongoDB query used to match the document to update.
|
|
237
|
+
* @param {UpdateFilter<Document> | Partial<Document>} data - The update operations or replacement document.
|
|
238
|
+
* @param {UpdateOptions} [options] - Optional settings for the update operation.
|
|
239
|
+
* @returns {Promise<UpdateResult>} A promise resolving to the result of the update operation.
|
|
240
|
+
*
|
|
241
|
+
* @throws {Error} If the user is not authorized to update the document.
|
|
242
|
+
*
|
|
243
|
+
* @description
|
|
244
|
+
* If `run_as_system` is enabled, the function directly updates the document using `collection.updateOne(query, data, options)`.
|
|
245
|
+
* Otherwise, it follows these steps:
|
|
246
|
+
* - Applies access control filters to the query using `getFormattedQuery`.
|
|
247
|
+
* - Retrieves the document using `findOne` to check if it exists and whether the user has permission to modify it.
|
|
248
|
+
* - Determines the user's role via `getWinningRole`.
|
|
249
|
+
* - Flattens update operators (`$set`, `$inc`, etc.) if present to extract the final modified fields.
|
|
250
|
+
* - Validates the update data using `checkValidation` to ensure compliance with role-based rules.
|
|
251
|
+
* - Ensures that no unauthorized modifications occur by comparing the validated document with the intended changes.
|
|
252
|
+
* - If validation fails, throws an error; otherwise, updates the document.
|
|
253
|
+
*/
|
|
254
|
+
updateOne: async (query, data, options) => {
|
|
255
|
+
if (!run_as_system) {
|
|
256
|
+
|
|
257
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
258
|
+
// Apply access control filters
|
|
259
|
+
|
|
260
|
+
// Normalize _id
|
|
261
|
+
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
262
|
+
const safeQuery = Array.isArray(formattedQuery)
|
|
263
|
+
? normalizeQuery(formattedQuery)
|
|
264
|
+
: formattedQuery
|
|
265
|
+
|
|
266
|
+
const result = await collection.findOne({ $and: safeQuery })
|
|
267
|
+
|
|
268
|
+
if (!result) {
|
|
269
|
+
throw new Error('Update not permitted')
|
|
270
|
+
}
|
|
257
271
|
|
|
258
|
-
|
|
272
|
+
const winningRole = getWinningRole(result, user, roles)
|
|
259
273
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
274
|
+
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
275
|
+
const hasOperators = Object.keys(data).some((key) => key.startsWith('$'))
|
|
263
276
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Flatten the update object to extract the actual fields being modified
|
|
270
|
-
// const docToCheck = hasOperators
|
|
271
|
-
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
272
|
-
// : data
|
|
273
|
-
const pipeline = [
|
|
274
|
-
{
|
|
275
|
-
$match: { $and: safeQuery }
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
$limit: 1
|
|
279
|
-
},
|
|
280
|
-
...Object.entries(data).map(([key, value]) => ({ [key]: value }))
|
|
281
|
-
]
|
|
282
|
-
const [docToCheck] = hasOperators
|
|
283
|
-
? await collection.aggregate(pipeline).toArray()
|
|
284
|
-
: ([data] as [Document])
|
|
285
|
-
// Validate update permissions
|
|
286
|
-
const { status, document } = winningRole
|
|
287
|
-
? await checkValidation(
|
|
288
|
-
winningRole,
|
|
277
|
+
// Flatten the update object to extract the actual fields being modified
|
|
278
|
+
// const docToCheck = hasOperators
|
|
279
|
+
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
280
|
+
// : data
|
|
281
|
+
const pipeline = [
|
|
289
282
|
{
|
|
290
|
-
|
|
291
|
-
roles,
|
|
292
|
-
cursor: docToCheck,
|
|
293
|
-
expansions: {}
|
|
283
|
+
$match: { $and: safeQuery }
|
|
294
284
|
},
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
* - Executes the query using `collection.find` with a `$and` of all filters.
|
|
319
|
-
* - Returns a cloned `FindCursor` where `toArray()`:
|
|
320
|
-
* - Applies additional post-query validation using `checkValidation` for each document.
|
|
321
|
-
* - Filters out documents the current user is not authorized to read.
|
|
322
|
-
*
|
|
323
|
-
* This ensures that both pre-query filtering and post-query validation are applied consistently.
|
|
324
|
-
*/
|
|
325
|
-
find: (query) => {
|
|
326
|
-
if (!run_as_system) {
|
|
327
|
-
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
328
|
-
// Pre-query filtering based on access control rules
|
|
329
|
-
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
330
|
-
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
|
|
331
|
-
// aggiunto filter per evitare questo errore: $and argument's entries must be objects
|
|
332
|
-
const cursor = collection.find(currentQuery)
|
|
333
|
-
const originalToArray = cursor.toArray.bind(cursor)
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Overridden `toArray` method that validates each document for read access.
|
|
337
|
-
*
|
|
338
|
-
* @returns {Promise<Document[]>} An array of documents the user is authorized to read.
|
|
339
|
-
*/
|
|
340
|
-
cursor.toArray = async () => {
|
|
341
|
-
const response = await originalToArray()
|
|
342
|
-
|
|
343
|
-
const filteredResponse = await Promise.all(
|
|
344
|
-
response.map(async (currentDoc) => {
|
|
345
|
-
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
346
|
-
|
|
347
|
-
logDebug('find winningRole', {
|
|
348
|
-
collection: collName,
|
|
349
|
-
userId: getUserId(user),
|
|
350
|
-
winningRoleName: winningRole?.name ?? null,
|
|
351
|
-
rolesLength: roles.length
|
|
352
|
-
})
|
|
353
|
-
const { status, document } = winningRole
|
|
354
|
-
? await checkValidation(
|
|
355
|
-
winningRole,
|
|
356
|
-
{
|
|
357
|
-
type: 'read',
|
|
358
|
-
roles,
|
|
359
|
-
cursor: currentDoc,
|
|
360
|
-
expansions: {}
|
|
361
|
-
},
|
|
362
|
-
user
|
|
363
|
-
)
|
|
364
|
-
: fallbackAccess(currentDoc)
|
|
365
|
-
|
|
366
|
-
return status ? document : undefined
|
|
367
|
-
})
|
|
368
|
-
)
|
|
285
|
+
{
|
|
286
|
+
$limit: 1
|
|
287
|
+
},
|
|
288
|
+
...Object.entries(data).map(([key, value]) => ({ [key]: value }))
|
|
289
|
+
]
|
|
290
|
+
const [docToCheck] = hasOperators
|
|
291
|
+
? await collection.aggregate(pipeline).toArray()
|
|
292
|
+
: ([data] as [Document])
|
|
293
|
+
// Validate update permissions
|
|
294
|
+
const { status, document } = winningRole
|
|
295
|
+
? await checkValidation(
|
|
296
|
+
winningRole,
|
|
297
|
+
{
|
|
298
|
+
type: 'write',
|
|
299
|
+
roles,
|
|
300
|
+
cursor: docToCheck,
|
|
301
|
+
expansions: {}
|
|
302
|
+
},
|
|
303
|
+
user
|
|
304
|
+
)
|
|
305
|
+
: fallbackAccess(docToCheck)
|
|
306
|
+
// Ensure no unauthorized changes are made
|
|
307
|
+
const areDocumentsEqual = isEqual(document, docToCheck)
|
|
369
308
|
|
|
370
|
-
|
|
309
|
+
if (!status || !areDocumentsEqual) {
|
|
310
|
+
throw new Error('Update not permitted')
|
|
311
|
+
}
|
|
312
|
+
return collection.updateOne({ $and: safeQuery }, data, options)
|
|
371
313
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// Apply access filters to initial change stream pipeline
|
|
401
|
-
const formattedQuery = getFormattedQuery(filters, {}, user)
|
|
402
|
-
|
|
403
|
-
const firstStep = formattedQuery.length ? {
|
|
404
|
-
$match: {
|
|
405
|
-
$and: formattedQuery
|
|
314
|
+
return collection.updateOne(query, data, options)
|
|
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')
|
|
406
342
|
}
|
|
407
|
-
} : undefined
|
|
408
343
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const isValidChange = async ({ fullDocument, updateDescription }: Document) => {
|
|
424
|
-
const winningRole = getWinningRole(fullDocument, user, roles)
|
|
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])
|
|
425
358
|
|
|
426
359
|
const { status, document } = winningRole
|
|
427
360
|
? await checkValidation(
|
|
428
361
|
winningRole,
|
|
429
362
|
{
|
|
430
|
-
type: '
|
|
363
|
+
type: 'write',
|
|
431
364
|
roles,
|
|
432
|
-
cursor:
|
|
365
|
+
cursor: docToCheck,
|
|
433
366
|
expansions: {}
|
|
434
367
|
},
|
|
435
368
|
user
|
|
436
369
|
)
|
|
437
|
-
: fallbackAccess(
|
|
370
|
+
: fallbackAccess(docToCheck)
|
|
371
|
+
|
|
372
|
+
const areDocumentsEqual = isEqual(document, docToCheck)
|
|
373
|
+
if (!status || !areDocumentsEqual) {
|
|
374
|
+
throw new Error('Update not permitted')
|
|
375
|
+
}
|
|
438
376
|
|
|
439
|
-
const
|
|
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
|
|
440
386
|
? await checkValidation(
|
|
441
|
-
|
|
387
|
+
readRole,
|
|
442
388
|
{
|
|
443
389
|
type: 'read',
|
|
444
390
|
roles,
|
|
445
|
-
cursor:
|
|
391
|
+
cursor: updateResult,
|
|
446
392
|
expansions: {}
|
|
447
393
|
},
|
|
448
394
|
user
|
|
449
395
|
)
|
|
450
|
-
: fallbackAccess(
|
|
396
|
+
: fallbackAccess(updateResult)
|
|
451
397
|
|
|
452
|
-
|
|
398
|
+
const sanitizedDoc = readResult.status ? (readResult.document ?? updateResult) : {}
|
|
399
|
+
return sanitizedDoc
|
|
453
400
|
}
|
|
454
401
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
402
|
+
return options
|
|
403
|
+
? collection.findOneAndUpdate(query, data, options)
|
|
404
|
+
: collection.findOneAndUpdate(query, data)
|
|
405
|
+
},
|
|
406
|
+
/**
|
|
407
|
+
* Finds documents in a MongoDB collection with optional role-based access control and post-query validation.
|
|
408
|
+
*
|
|
409
|
+
* @param {Filter<Document>} query - The MongoDB query to filter documents.
|
|
410
|
+
* @returns {FindCursor} A customized `FindCursor` that includes additional access control logic in its `toArray()` method.
|
|
411
|
+
*
|
|
412
|
+
* @description
|
|
413
|
+
* If `run_as_system` is enabled, the function simply returns a regular MongoDB cursor (`collection.find(query)`).
|
|
414
|
+
* Otherwise:
|
|
415
|
+
* - Combines the user query with role-based filters via `getFormattedQuery`.
|
|
416
|
+
* - Executes the query using `collection.find` with a `$and` of all filters.
|
|
417
|
+
* - Returns a cloned `FindCursor` where `toArray()`:
|
|
418
|
+
* - Applies additional post-query validation using `checkValidation` for each document.
|
|
419
|
+
* - Filters out documents the current user is not authorized to read.
|
|
420
|
+
*
|
|
421
|
+
* This ensures that both pre-query filtering and post-query validation are applied consistently.
|
|
422
|
+
*/
|
|
423
|
+
find: (query) => {
|
|
424
|
+
if (!run_as_system) {
|
|
425
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
426
|
+
// Pre-query filtering based on access control rules
|
|
427
|
+
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
428
|
+
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
|
|
429
|
+
// aggiunto filter per evitare questo errore: $and argument's entries must be objects
|
|
430
|
+
const cursor = collection.find(currentQuery)
|
|
431
|
+
const originalToArray = cursor.toArray.bind(cursor)
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Overridden `toArray` method that validates each document for read access.
|
|
435
|
+
*
|
|
436
|
+
* @returns {Promise<Document[]>} An array of documents the user is authorized to read.
|
|
437
|
+
*/
|
|
438
|
+
cursor.toArray = async () => {
|
|
439
|
+
const response = await originalToArray()
|
|
440
|
+
|
|
441
|
+
const filteredResponse = await Promise.all(
|
|
442
|
+
response.map(async (currentDoc) => {
|
|
443
|
+
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
444
|
+
|
|
445
|
+
logDebug('find winningRole', {
|
|
446
|
+
collection: collName,
|
|
447
|
+
userId: getUserId(user),
|
|
448
|
+
winningRoleName: winningRole?.name ?? null,
|
|
449
|
+
rolesLength: roles.length
|
|
450
|
+
})
|
|
451
|
+
const { status, document } = winningRole
|
|
452
|
+
? await checkValidation(
|
|
453
|
+
winningRole,
|
|
454
|
+
{
|
|
455
|
+
type: 'read',
|
|
456
|
+
roles,
|
|
457
|
+
cursor: currentDoc,
|
|
458
|
+
expansions: {}
|
|
459
|
+
},
|
|
460
|
+
user
|
|
461
|
+
)
|
|
462
|
+
: fallbackAccess(currentDoc)
|
|
463
|
+
|
|
464
|
+
return status ? document : undefined
|
|
465
|
+
})
|
|
466
|
+
)
|
|
473
467
|
|
|
474
|
-
|
|
475
|
-
}
|
|
468
|
+
return filteredResponse.filter(Boolean) as WithId<Document>[]
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return cursor
|
|
476
472
|
}
|
|
477
|
-
return
|
|
478
|
-
|
|
473
|
+
// System mode: return original unfiltered cursor
|
|
474
|
+
return collection.find(query)
|
|
475
|
+
},
|
|
476
|
+
/**
|
|
477
|
+
* Watches changes on a MongoDB collection with optional role-based filtering of change events.
|
|
478
|
+
*
|
|
479
|
+
* @param {Document[]} [pipeline=[]] - Optional aggregation pipeline stages to apply to the change stream.
|
|
480
|
+
* @param {ChangeStreamOptions} [options] - Optional settings for the change stream, such as `fullDocument`, `resumeAfter`, etc.
|
|
481
|
+
* @returns {ChangeStream} A MongoDB `ChangeStream` instance, optionally enhanced with access control.
|
|
482
|
+
*
|
|
483
|
+
* @description
|
|
484
|
+
* If `run_as_system` is enabled, this function simply returns `collection.watch(pipeline, options)`.
|
|
485
|
+
* Otherwise:
|
|
486
|
+
* - Applies access control filters via `getFormattedQuery`.
|
|
487
|
+
* - Prepends a `$match` stage to the pipeline to limit watched changes to authorized documents.
|
|
488
|
+
* - Overrides the `.on()` method of the returned `ChangeStream` to:
|
|
489
|
+
* - Validate the `fullDocument` and any `updatedFields` using `checkValidation`.
|
|
490
|
+
* - Filter out change events the user is not authorized to see.
|
|
491
|
+
* - Pass only validated and filtered events to the original listener.
|
|
492
|
+
*
|
|
493
|
+
* This allows fine-grained control over what change events a user can observe, based on roles and filters.
|
|
494
|
+
*/
|
|
495
|
+
watch: (pipeline = [], options) => {
|
|
496
|
+
if (!run_as_system) {
|
|
497
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
498
|
+
// Apply access filters to initial change stream pipeline
|
|
499
|
+
const formattedQuery = getFormattedQuery(filters, {}, user)
|
|
500
|
+
|
|
501
|
+
const firstStep = formattedQuery.length ? {
|
|
502
|
+
$match: {
|
|
503
|
+
$and: formattedQuery
|
|
504
|
+
}
|
|
505
|
+
} : undefined
|
|
479
506
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
aggregate: async (pipeline = [], options, isClient) => {
|
|
485
|
-
if (run_as_system || !isClient) {
|
|
486
|
-
return collection.aggregate(pipeline, options)
|
|
487
|
-
}
|
|
507
|
+
const formattedPipeline = [
|
|
508
|
+
firstStep,
|
|
509
|
+
...pipeline
|
|
510
|
+
].filter(Boolean) as Document[]
|
|
488
511
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
})
|
|
501
|
-
const projection = getFormattedProjection(filters)
|
|
502
|
-
const hiddenFields = getHiddenFieldsFromRulesConfig(rulesConfig)
|
|
503
|
-
|
|
504
|
-
const sanitizedPipeline = applyAccessControlToPipeline(
|
|
505
|
-
pipeline,
|
|
506
|
-
normalizedRules,
|
|
507
|
-
user,
|
|
508
|
-
collName,
|
|
509
|
-
{ isClientPipeline: true }
|
|
510
|
-
)
|
|
511
|
-
logDebug('aggregate sanitizedPipeline', {
|
|
512
|
-
collection: collName,
|
|
513
|
-
sanitizedPipeline
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
const guardedPipeline = [
|
|
517
|
-
...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
|
|
518
|
-
...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
|
|
519
|
-
...(projection ? [{ $project: projection }] : []),
|
|
520
|
-
...sanitizedPipeline
|
|
521
|
-
]
|
|
522
|
-
|
|
523
|
-
const originalCursor = collection.aggregate(guardedPipeline, options)
|
|
524
|
-
const newCursor = Object.create(originalCursor)
|
|
525
|
-
|
|
526
|
-
newCursor.toArray = async () => originalCursor.toArray()
|
|
527
|
-
|
|
528
|
-
return newCursor
|
|
529
|
-
},
|
|
530
|
-
/**
|
|
531
|
-
* Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
|
|
532
|
-
*
|
|
533
|
-
* @param {OptionalId<Document>[]} documents - The array of documents to insert.
|
|
534
|
-
* @param {BulkWriteOptions} [options] - Optional settings passed to `insertMany`, such as `ordered`, `writeConcern`, etc.
|
|
535
|
-
* @returns {Promise<InsertManyResult<Document>>} A promise resolving to the result of the insert operation.
|
|
536
|
-
*
|
|
537
|
-
* @throws {Error} If no documents pass validation or user is not permitted to insert.
|
|
538
|
-
*
|
|
539
|
-
* @description
|
|
540
|
-
* If `run_as_system` is enabled, this function directly inserts the documents without validation.
|
|
541
|
-
* Otherwise, for each document:
|
|
542
|
-
* - Finds the user's applicable role using `getWinningRole`.
|
|
543
|
-
* - Validates the insert operation through `checkValidation`.
|
|
544
|
-
* - Filters out any documents the user is not authorized to insert.
|
|
545
|
-
* Only documents passing validation will be inserted.
|
|
546
|
-
*/
|
|
547
|
-
insertMany: async (documents, options) => {
|
|
548
|
-
if (!run_as_system) {
|
|
549
|
-
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
|
|
550
|
-
// Validate each document against user's roles
|
|
551
|
-
const filteredItems = await Promise.all(
|
|
552
|
-
documents.map(async (currentDoc) => {
|
|
553
|
-
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
512
|
+
const result = collection.watch(formattedPipeline, options)
|
|
513
|
+
const originalOn = result.on.bind(result)
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Validates a change event against the user's roles.
|
|
517
|
+
*
|
|
518
|
+
* @param {Document} change - A change event from the ChangeStream.
|
|
519
|
+
* @returns {Promise<{ status: boolean, document: Document, updatedFieldsStatus: boolean, updatedFields: Document }>}
|
|
520
|
+
*/
|
|
521
|
+
const isValidChange = async ({ fullDocument, updateDescription }: Document) => {
|
|
522
|
+
const winningRole = getWinningRole(fullDocument, user, roles)
|
|
554
523
|
|
|
555
524
|
const { status, document } = winningRole
|
|
556
525
|
? await checkValidation(
|
|
557
526
|
winningRole,
|
|
558
527
|
{
|
|
559
|
-
type: '
|
|
528
|
+
type: 'read',
|
|
560
529
|
roles,
|
|
561
|
-
cursor:
|
|
530
|
+
cursor: fullDocument,
|
|
562
531
|
expansions: {}
|
|
563
532
|
},
|
|
564
533
|
user
|
|
565
534
|
)
|
|
566
|
-
|
|
535
|
+
: fallbackAccess(fullDocument)
|
|
567
536
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
537
|
+
const { status: updatedFieldsStatus, document: updatedFields } = winningRole
|
|
538
|
+
? await checkValidation(
|
|
539
|
+
winningRole,
|
|
540
|
+
{
|
|
541
|
+
type: 'read',
|
|
542
|
+
roles,
|
|
543
|
+
cursor: updateDescription?.updatedFields,
|
|
544
|
+
expansions: {}
|
|
545
|
+
},
|
|
546
|
+
user
|
|
547
|
+
)
|
|
548
|
+
: fallbackAccess(updateDescription?.updatedFields)
|
|
549
|
+
|
|
550
|
+
return { status, document, updatedFieldsStatus, updatedFields }
|
|
551
|
+
}
|
|
571
552
|
|
|
572
|
-
|
|
553
|
+
// Override the .on() method to apply validation before emitting events
|
|
554
|
+
result.on = <EventKey extends keyof EventsDescription>(
|
|
555
|
+
eventType: EventKey,
|
|
556
|
+
listener: EventsDescription[EventKey]
|
|
557
|
+
) => {
|
|
558
|
+
return originalOn(eventType, async (change: Document) => {
|
|
559
|
+
const { status, document, updatedFieldsStatus, updatedFields } =
|
|
560
|
+
await isValidChange(change)
|
|
561
|
+
if (!status) return
|
|
562
|
+
|
|
563
|
+
const filteredChange = {
|
|
564
|
+
...change,
|
|
565
|
+
fullDocument: document,
|
|
566
|
+
updateDescription: {
|
|
567
|
+
...change.updateDescription,
|
|
568
|
+
updatedFields: updatedFieldsStatus ? updatedFields : {}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
573
571
|
|
|
574
|
-
|
|
575
|
-
|
|
572
|
+
listener(filteredChange)
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
return result
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
586
|
-
// Apply access control filters
|
|
587
|
-
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
588
|
-
|
|
589
|
-
// Retrieve the document to check permissions before updating
|
|
590
|
-
const result = await collection.find({ $and: formattedQuery }).toArray()
|
|
591
|
-
if (!result) {
|
|
592
|
-
console.log('check1 In updateMany --> (!result)')
|
|
593
|
-
throw new Error('Update not permitted')
|
|
578
|
+
// System mode: no filtering applied
|
|
579
|
+
return collection.watch(pipeline, options)
|
|
580
|
+
},
|
|
581
|
+
//TODO -> add filter & rules in aggregate
|
|
582
|
+
aggregate: (pipeline = [], options, isClient) => {
|
|
583
|
+
if (run_as_system || !isClient) {
|
|
584
|
+
return collection.aggregate(pipeline, options)
|
|
594
585
|
}
|
|
595
586
|
|
|
596
|
-
|
|
597
|
-
|
|
587
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
588
|
+
|
|
589
|
+
const rulesConfig = collectionRules ?? { filters, roles }
|
|
598
590
|
|
|
599
|
-
|
|
600
|
-
// const docToCheck = hasOperators
|
|
601
|
-
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
602
|
-
// : data
|
|
591
|
+
ensureClientPipelineStages(pipeline)
|
|
603
592
|
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
593
|
+
const formattedQuery = getFormattedQuery(filters, {}, user)
|
|
594
|
+
logDebug('aggregate formattedQuery', {
|
|
595
|
+
collection: collName,
|
|
596
|
+
formattedQuery,
|
|
597
|
+
pipeline
|
|
598
|
+
})
|
|
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
|
+
})
|
|
613
|
+
|
|
614
|
+
const guardedPipeline = [
|
|
615
|
+
...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
|
|
616
|
+
...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
|
|
617
|
+
...(projection ? [{ $project: projection }] : []),
|
|
618
|
+
...sanitizedPipeline
|
|
609
619
|
]
|
|
610
620
|
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
: result
|
|
621
|
+
const originalCursor = collection.aggregate(guardedPipeline, options)
|
|
622
|
+
const newCursor = Object.create(originalCursor)
|
|
614
623
|
|
|
615
|
-
|
|
616
|
-
docsToCheck.map(async (currentDoc) => {
|
|
617
|
-
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
624
|
+
newCursor.toArray = async () => originalCursor.toArray()
|
|
618
625
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
626
|
+
return newCursor
|
|
627
|
+
},
|
|
628
|
+
/**
|
|
629
|
+
* Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
|
|
630
|
+
*
|
|
631
|
+
* @param {OptionalId<Document>[]} documents - The array of documents to insert.
|
|
632
|
+
* @param {BulkWriteOptions} [options] - Optional settings passed to `insertMany`, such as `ordered`, `writeConcern`, etc.
|
|
633
|
+
* @returns {Promise<InsertManyResult<Document>>} A promise resolving to the result of the insert operation.
|
|
634
|
+
*
|
|
635
|
+
* @throws {Error} If no documents pass validation or user is not permitted to insert.
|
|
636
|
+
*
|
|
637
|
+
* @description
|
|
638
|
+
* If `run_as_system` is enabled, this function directly inserts the documents without validation.
|
|
639
|
+
* Otherwise, for each document:
|
|
640
|
+
* - Finds the user's applicable role using `getWinningRole`.
|
|
641
|
+
* - Validates the insert operation through `checkValidation`.
|
|
642
|
+
* - Filters out any documents the user is not authorized to insert.
|
|
643
|
+
* Only documents passing validation will be inserted.
|
|
644
|
+
*/
|
|
645
|
+
insertMany: async (documents, options) => {
|
|
646
|
+
if (!run_as_system) {
|
|
647
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
|
|
648
|
+
// Validate each document against user's roles
|
|
649
|
+
const filteredItems = await Promise.all(
|
|
650
|
+
documents.map(async (currentDoc) => {
|
|
651
|
+
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
631
652
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
653
|
+
const { status, document } = winningRole
|
|
654
|
+
? await checkValidation(
|
|
655
|
+
winningRole,
|
|
656
|
+
{
|
|
657
|
+
type: 'insert',
|
|
658
|
+
roles,
|
|
659
|
+
cursor: currentDoc,
|
|
660
|
+
expansions: {}
|
|
661
|
+
},
|
|
662
|
+
user
|
|
663
|
+
)
|
|
664
|
+
: fallbackAccess(currentDoc)
|
|
665
|
+
|
|
666
|
+
return status ? document : undefined
|
|
667
|
+
})
|
|
668
|
+
)
|
|
635
669
|
|
|
636
|
-
|
|
637
|
-
const areDocumentsEqual = isEqual(docsToCheck, filteredItems)
|
|
670
|
+
const canInsert = isEqual(filteredItems, documents)
|
|
638
671
|
|
|
639
|
-
|
|
640
|
-
|
|
672
|
+
if (!canInsert) {
|
|
673
|
+
throw new Error('Insert not permitted')
|
|
674
|
+
}
|
|
641
675
|
|
|
642
|
-
|
|
676
|
+
return collection.insertMany(documents, options)
|
|
643
677
|
}
|
|
678
|
+
// If system mode is active, insert all documents without validation
|
|
679
|
+
return collection.insertMany(documents, options)
|
|
680
|
+
},
|
|
681
|
+
updateMany: async (query, data, options) => {
|
|
682
|
+
if (!run_as_system) {
|
|
683
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
684
|
+
// Apply access control filters
|
|
685
|
+
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
686
|
+
|
|
687
|
+
// Retrieve the document to check permissions before updating
|
|
688
|
+
const result = await collection.find({ $and: formattedQuery }).toArray()
|
|
689
|
+
if (!result) {
|
|
690
|
+
console.log('check1 In updateMany --> (!result)')
|
|
691
|
+
throw new Error('Update not permitted')
|
|
692
|
+
}
|
|
644
693
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
return collection.updateMany(query, data, options)
|
|
648
|
-
},
|
|
649
|
-
/**
|
|
650
|
-
* Deletes multiple documents from a MongoDB collection with role-based access control and validation.
|
|
651
|
-
*
|
|
652
|
-
* @param query - The initial MongoDB query to filter documents to be deleted.
|
|
653
|
-
* @returns {Promise<{ acknowledged: boolean, deletedCount: number }>} A promise resolving to the deletion result.
|
|
654
|
-
*
|
|
655
|
-
* @description
|
|
656
|
-
* If `run_as_system` is enabled, this function directly deletes documents matching the given query.
|
|
657
|
-
* Otherwise, it:
|
|
658
|
-
* - Applies additional filters from access control rules.
|
|
659
|
-
* - Fetches matching documents.
|
|
660
|
-
* - Validates each document against user roles.
|
|
661
|
-
* - Deletes only the documents that the current user has permission to delete.
|
|
662
|
-
*/
|
|
663
|
-
deleteMany: async (query = {}) => {
|
|
664
|
-
if (!run_as_system) {
|
|
665
|
-
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
|
|
666
|
-
// Apply access control filters
|
|
667
|
-
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
694
|
+
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
695
|
+
const hasOperators = Object.keys(data).some((key) => key.startsWith('$'))
|
|
668
696
|
|
|
669
|
-
|
|
670
|
-
|
|
697
|
+
// Flatten the update object to extract the actual fields being modified
|
|
698
|
+
// const docToCheck = hasOperators
|
|
699
|
+
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
700
|
+
// : data
|
|
671
701
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
702
|
+
const pipeline = [
|
|
703
|
+
{
|
|
704
|
+
$match: { $and: formattedQuery }
|
|
705
|
+
},
|
|
706
|
+
...Object.entries(data).map(([key, value]) => ({ [key]: value }))
|
|
707
|
+
]
|
|
676
708
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
{
|
|
681
|
-
type: 'delete',
|
|
682
|
-
roles,
|
|
683
|
-
cursor: currentDoc,
|
|
684
|
-
expansions: {}
|
|
685
|
-
},
|
|
686
|
-
user
|
|
687
|
-
)
|
|
688
|
-
: fallbackAccess(currentDoc)
|
|
709
|
+
const docsToCheck = hasOperators
|
|
710
|
+
? await collection.aggregate(pipeline).toArray()
|
|
711
|
+
: result
|
|
689
712
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
713
|
+
const filteredItems = await Promise.all(
|
|
714
|
+
docsToCheck.map(async (currentDoc) => {
|
|
715
|
+
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
693
716
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
717
|
+
const { status, document } = winningRole
|
|
718
|
+
? await checkValidation(
|
|
719
|
+
winningRole,
|
|
720
|
+
{
|
|
721
|
+
type: 'write',
|
|
722
|
+
roles,
|
|
723
|
+
cursor: currentDoc,
|
|
724
|
+
expansions: {}
|
|
725
|
+
},
|
|
726
|
+
user
|
|
727
|
+
)
|
|
728
|
+
: fallbackAccess(currentDoc)
|
|
698
729
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
730
|
+
return status ? document : undefined
|
|
731
|
+
})
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
// Ensure no unauthorized changes are made
|
|
735
|
+
const areDocumentsEqual = isEqual(docsToCheck, filteredItems)
|
|
736
|
+
|
|
737
|
+
if (!areDocumentsEqual) {
|
|
738
|
+
console.log('check1 In updateMany --> (!areDocumentsEqual)')
|
|
739
|
+
|
|
740
|
+
throw new Error('Update not permitted')
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return collection.updateMany({ $and: formattedQuery }, data, options)
|
|
704
744
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
745
|
+
return collection.updateMany(query, data, options)
|
|
746
|
+
},
|
|
747
|
+
/**
|
|
748
|
+
* Deletes multiple documents from a MongoDB collection with role-based access control and validation.
|
|
749
|
+
*
|
|
750
|
+
* @param query - The initial MongoDB query to filter documents to be deleted.
|
|
751
|
+
* @returns {Promise<{ acknowledged: boolean, deletedCount: number }>} A promise resolving to the deletion result.
|
|
752
|
+
*
|
|
753
|
+
* @description
|
|
754
|
+
* If `run_as_system` is enabled, this function directly deletes documents matching the given query.
|
|
755
|
+
* Otherwise, it:
|
|
756
|
+
* - Applies additional filters from access control rules.
|
|
757
|
+
* - Fetches matching documents.
|
|
758
|
+
* - Validates each document against user roles.
|
|
759
|
+
* - Deletes only the documents that the current user has permission to delete.
|
|
760
|
+
*/
|
|
761
|
+
deleteMany: async (query = {}) => {
|
|
762
|
+
if (!run_as_system) {
|
|
763
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
|
|
764
|
+
// Apply access control filters
|
|
765
|
+
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
766
|
+
|
|
767
|
+
// Fetch documents matching the combined filters
|
|
768
|
+
const data = await collection.find({ $and: formattedQuery }).toArray()
|
|
769
|
+
|
|
770
|
+
// Filter and validate each document based on user's roles
|
|
771
|
+
const filteredItems = await Promise.all(
|
|
772
|
+
data.map(async (currentDoc) => {
|
|
773
|
+
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
774
|
+
|
|
775
|
+
const { status, document } = winningRole
|
|
776
|
+
? await checkValidation(
|
|
777
|
+
winningRole,
|
|
778
|
+
{
|
|
779
|
+
type: 'delete',
|
|
780
|
+
roles,
|
|
781
|
+
cursor: currentDoc,
|
|
782
|
+
expansions: {}
|
|
783
|
+
},
|
|
784
|
+
user
|
|
785
|
+
)
|
|
786
|
+
: fallbackAccess(currentDoc)
|
|
787
|
+
|
|
788
|
+
return status ? document : undefined
|
|
789
|
+
})
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
// Extract IDs of documents that passed validation
|
|
793
|
+
const elementsToDelete = (filteredItems.filter(Boolean) as WithId<Document>[]).map(
|
|
794
|
+
({ _id }) => _id
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
if (!elementsToDelete.length) {
|
|
798
|
+
return Promise.resolve({
|
|
799
|
+
acknowledged: true,
|
|
800
|
+
deletedCount: 0
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
// Build final delete query with access control and ID filter
|
|
804
|
+
const deleteQuery = {
|
|
805
|
+
$and: [...formattedQuery, { _id: { $in: elementsToDelete } }]
|
|
806
|
+
}
|
|
807
|
+
return collection.deleteMany(deleteQuery)
|
|
708
808
|
}
|
|
709
|
-
|
|
809
|
+
// If running as system, bypass access control and delete directly
|
|
810
|
+
return collection.deleteMany(query)
|
|
710
811
|
}
|
|
711
|
-
// If running as system, bypass access control and delete directly
|
|
712
|
-
return collection.deleteMany(query)
|
|
713
|
-
}
|
|
714
812
|
}
|
|
715
813
|
}
|
|
716
814
|
|