@flowerforce/flowerbase 1.2.0 → 1.2.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +3 -0
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +5 -2
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +7 -10
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +3 -2
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -1
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +28 -2
- package/dist/features/rules/utils.d.ts.map +1 -1
- package/dist/features/rules/utils.js +11 -2
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +52 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -9
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +540 -483
- package/dist/services/mongodb-atlas/utils.d.ts +9 -2
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +113 -23
- package/dist/shared/handleUserRegistration.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.js +1 -0
- package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
- package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
- package/dist/utils/context/helpers.d.ts +6 -5
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/helpers.js +3 -0
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +2 -0
- package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +11 -4
- package/dist/utils/initializer/registerPlugins.d.ts +3 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +9 -6
- package/dist/utils/roles/helpers.js +9 -2
- package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
- package/dist/utils/roles/machines/commonValidators.js +10 -6
- package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
- package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/B/validators.js +8 -0
- package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/C/index.js +10 -7
- package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
- package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/C/validators.js +29 -0
- package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/D/index.js +13 -11
- package/dist/utils/rules.d.ts +1 -1
- package/dist/utils/rules.d.ts.map +1 -1
- package/dist/utils/rules.js +26 -17
- package/jest.config.ts +2 -12
- package/jest.setup.ts +28 -0
- package/package.json +1 -1
- package/src/auth/controller.ts +3 -0
- package/src/auth/providers/custom-function/controller.ts +5 -2
- package/src/auth/providers/local-userpass/controller.ts +13 -10
- package/src/auth/utils.ts +6 -3
- package/src/constants.ts +7 -2
- package/src/fastify.d.ts +32 -15
- package/src/features/functions/controller.ts +36 -2
- package/src/features/rules/utils.ts +11 -2
- package/src/features/triggers/utils.ts +59 -2
- package/src/index.ts +21 -8
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
- package/src/services/mongodb-atlas/index.ts +143 -90
- package/src/services/mongodb-atlas/utils.ts +158 -22
- package/src/shared/handleUserRegistration.ts +3 -3
- package/src/shared/models/handleUserRegistration.model.ts +8 -3
- package/src/types/fastify-raw-body.d.ts +22 -0
- package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
- package/src/utils/__tests__/registerPlugins.test.ts +16 -1
- package/src/utils/context/helpers.ts +3 -0
- package/src/utils/context/index.ts +1 -0
- package/src/utils/initializer/exposeRoutes.ts +15 -8
- package/src/utils/initializer/registerPlugins.ts +15 -7
- package/src/utils/roles/helpers.ts +20 -3
- package/src/utils/roles/machines/commonValidators.ts +10 -5
- package/src/utils/roles/machines/read/B/validators.ts +8 -0
- package/src/utils/roles/machines/read/C/index.ts +11 -7
- package/src/utils/roles/machines/read/C/validators.ts +21 -0
- package/src/utils/roles/machines/read/D/index.ts +22 -12
- package/src/utils/rules.ts +31 -22
- package/tsconfig.spec.json +7 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ensureClientPipelineStages, getHiddenFieldsFromRulesConfig, prependUnsetStage, applyAccessControlToPipeline } from '../utils'
|
|
2
|
+
import { Role } from '../../../utils/roles/interface'
|
|
3
|
+
|
|
4
|
+
describe('MongoDB Atlas aggregate helpers', () => {
|
|
5
|
+
describe('ensureClientPipelineStages', () => {
|
|
6
|
+
it('allows safe stages', () => {
|
|
7
|
+
expect(() =>
|
|
8
|
+
ensureClientPipelineStages([{ $match: { active: true } }])
|
|
9
|
+
).not.toThrow()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('throws when unsupported stage is used', () => {
|
|
13
|
+
expect(() =>
|
|
14
|
+
ensureClientPipelineStages([{ $replaceRoot: { newRoot: '$$ROOT' } }])
|
|
15
|
+
).toThrow('Stage $replaceRoot is not allowed in client aggregate pipelines')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('recurses into nested lookups and facets without throwing', () => {
|
|
19
|
+
const pipeline = [
|
|
20
|
+
{
|
|
21
|
+
$lookup: {
|
|
22
|
+
from: 'other',
|
|
23
|
+
localField: 'ref',
|
|
24
|
+
foreignField: '_id',
|
|
25
|
+
as: 'joined',
|
|
26
|
+
pipeline: [
|
|
27
|
+
{
|
|
28
|
+
$facet: {
|
|
29
|
+
safe: [{ $match: { foo: 'bar' } }]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
expect(() => ensureClientPipelineStages(pipeline)).not.toThrow()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('getHiddenFieldsFromRulesConfig', () => {
|
|
42
|
+
it('returns fields marked as unreadable', () => {
|
|
43
|
+
const roles: Role[] = [
|
|
44
|
+
{
|
|
45
|
+
name: 'demo',
|
|
46
|
+
apply_when: {},
|
|
47
|
+
insert: true,
|
|
48
|
+
delete: true,
|
|
49
|
+
search: true,
|
|
50
|
+
read: true,
|
|
51
|
+
write: true,
|
|
52
|
+
fields: {
|
|
53
|
+
secret: { read: false, write: false },
|
|
54
|
+
visible: { read: true, write: false }
|
|
55
|
+
},
|
|
56
|
+
additional_fields: {
|
|
57
|
+
hiddenExtra: { read: false, write: false }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const hidden = getHiddenFieldsFromRulesConfig({
|
|
63
|
+
roles
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(hidden).toEqual(expect.arrayContaining(['secret', 'hiddenExtra']))
|
|
67
|
+
expect(hidden).not.toContain('visible')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('prependUnsetStage', () => {
|
|
72
|
+
it('inserts an $unset stage when hidden fields are present', () => {
|
|
73
|
+
const pipeline = [{ $match: { active: true } }]
|
|
74
|
+
const result = prependUnsetStage(pipeline, ['password', 'secret'])
|
|
75
|
+
|
|
76
|
+
expect(result[0]).toEqual({ $unset: ['password', 'secret'] })
|
|
77
|
+
expect(result[1]).toEqual(pipeline[0])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('returns original pipeline if no hidden fields exist', () => {
|
|
81
|
+
const pipeline = [{ $match: { active: true } }]
|
|
82
|
+
expect(prependUnsetStage(pipeline, [])).toEqual(pipeline)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('applyAccessControlToPipeline', () => {
|
|
87
|
+
it('prepends hidden-field $unset inside lookup pipelines for client requests', () => {
|
|
88
|
+
const rules = {
|
|
89
|
+
main: {
|
|
90
|
+
filters: [],
|
|
91
|
+
roles: []
|
|
92
|
+
},
|
|
93
|
+
other: {
|
|
94
|
+
filters: [],
|
|
95
|
+
roles: [
|
|
96
|
+
{
|
|
97
|
+
name: 'lookup-role',
|
|
98
|
+
apply_when: {},
|
|
99
|
+
insert: true,
|
|
100
|
+
delete: true,
|
|
101
|
+
search: true,
|
|
102
|
+
read: true,
|
|
103
|
+
write: true,
|
|
104
|
+
fields: {
|
|
105
|
+
secretField: { read: false, write: false }
|
|
106
|
+
},
|
|
107
|
+
additional_fields: {
|
|
108
|
+
secretAux: { read: false, write: false }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pipeline = [
|
|
116
|
+
{
|
|
117
|
+
$lookup: {
|
|
118
|
+
from: 'other',
|
|
119
|
+
localField: 'ref',
|
|
120
|
+
foreignField: '_id',
|
|
121
|
+
as: 'joined',
|
|
122
|
+
pipeline: [{ $match: { active: true } }]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
const sanitized = applyAccessControlToPipeline(
|
|
128
|
+
pipeline,
|
|
129
|
+
rules,
|
|
130
|
+
{},
|
|
131
|
+
'main',
|
|
132
|
+
{ isClientPipeline: true }
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const lookupPipeline = sanitized[0].$lookup.pipeline
|
|
136
|
+
expect(lookupPipeline?.[0]).toEqual({
|
|
137
|
+
$unset: ['secretField', 'secretAux']
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -1,23 +1,54 @@
|
|
|
1
|
-
import { EventEmitterAsyncResourceOptions } from 'events'
|
|
2
1
|
import isEqual from 'lodash/isEqual'
|
|
3
|
-
import { Collection, Document, EventsDescription,
|
|
2
|
+
import { Collection, Document, EventsDescription, WithId } from 'mongodb'
|
|
4
3
|
import { checkValidation } from '../../utils/roles/machines'
|
|
5
4
|
import { getWinningRole } from '../../utils/roles/machines/utils'
|
|
6
5
|
import { CRUD_OPERATIONS, GetOperatorsFunction, MongodbAtlasFunction } from './model'
|
|
7
6
|
import {
|
|
8
7
|
applyAccessControlToPipeline,
|
|
9
8
|
checkDenyOperation,
|
|
9
|
+
ensureClientPipelineStages,
|
|
10
10
|
getFormattedProjection,
|
|
11
11
|
getFormattedQuery,
|
|
12
|
+
getHiddenFieldsFromRulesConfig,
|
|
12
13
|
normalizeQuery
|
|
13
14
|
} from './utils'
|
|
15
|
+
import { Rules } from '../../features/rules/interface'
|
|
14
16
|
|
|
15
17
|
//TODO aggiungere no-sql inject security
|
|
18
|
+
const debugRules = process.env.DEBUG_RULES === 'true'
|
|
19
|
+
const debugServices = process.env.DEBUG_SERVICES === 'true'
|
|
20
|
+
|
|
21
|
+
const logDebug = (message: string, payload?: unknown) => {
|
|
22
|
+
if (!debugRules) return
|
|
23
|
+
const formatted = payload && typeof payload === 'object' ? JSON.stringify(payload) : payload
|
|
24
|
+
console.log(`[rules-debug] ${message}`, formatted ?? '')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getUserId = (user?: unknown) => {
|
|
28
|
+
if (!user || typeof user !== 'object') return undefined
|
|
29
|
+
return (user as { id?: string }).id
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const logService = (message: string, payload?: unknown) => {
|
|
33
|
+
if (!debugServices) return
|
|
34
|
+
console.log('[service-debug]', message, payload ?? '')
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
const getOperators: GetOperatorsFunction = (
|
|
17
38
|
collection,
|
|
18
|
-
{ rules
|
|
19
|
-
) =>
|
|
20
|
-
|
|
39
|
+
{ rules, collName, user, run_as_system }
|
|
40
|
+
) => {
|
|
41
|
+
const normalizedRules: Rules = rules ?? ({} as Rules)
|
|
42
|
+
const collectionRules = normalizedRules[collName]
|
|
43
|
+
const filters = collectionRules?.filters ?? []
|
|
44
|
+
const roles = collectionRules?.roles ?? []
|
|
45
|
+
const fallbackAccess = (doc: Document | null | undefined = undefined) => ({
|
|
46
|
+
status: false,
|
|
47
|
+
document: doc
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
/**
|
|
21
52
|
* Finds a single document in a MongoDB collection with optional role-based filtering and validation.
|
|
22
53
|
*
|
|
23
54
|
* @param {Filter<Document>} query - The MongoDB query used to match the document.
|
|
@@ -34,16 +65,38 @@ const getOperators: GetOperatorsFunction = (
|
|
|
34
65
|
*/
|
|
35
66
|
findOne: async (query) => {
|
|
36
67
|
if (!run_as_system) {
|
|
37
|
-
checkDenyOperation(
|
|
38
|
-
const { filters, roles } = rules[collName] || {}
|
|
39
|
-
|
|
68
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
40
69
|
// Apply access control filters to the query
|
|
41
70
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
logDebug('update formattedQuery', {
|
|
72
|
+
collection: collName,
|
|
73
|
+
query,
|
|
74
|
+
formattedQuery
|
|
75
|
+
})
|
|
76
|
+
logDebug('find formattedQuery', {
|
|
77
|
+
collection: collName,
|
|
78
|
+
query,
|
|
79
|
+
formattedQuery,
|
|
80
|
+
rolesLength: roles.length
|
|
81
|
+
})
|
|
82
|
+
|
|
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 })
|
|
44
92
|
|
|
45
93
|
const winningRole = getWinningRole(result, user, roles)
|
|
46
94
|
|
|
95
|
+
logDebug('findOne winningRole', {
|
|
96
|
+
collection: collName,
|
|
97
|
+
winningRoleName: winningRole?.name ?? null,
|
|
98
|
+
userId: getUserId(user)
|
|
99
|
+
})
|
|
47
100
|
const { status, document } = winningRole
|
|
48
101
|
? await checkValidation(
|
|
49
102
|
winningRole,
|
|
@@ -55,7 +108,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
55
108
|
},
|
|
56
109
|
user
|
|
57
110
|
)
|
|
58
|
-
:
|
|
111
|
+
: fallbackAccess(result)
|
|
59
112
|
|
|
60
113
|
// Return validated document or empty object if not permitted
|
|
61
114
|
return Promise.resolve(status ? document : {})
|
|
@@ -82,9 +135,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
82
135
|
*/
|
|
83
136
|
deleteOne: async (query = {}) => {
|
|
84
137
|
if (!run_as_system) {
|
|
85
|
-
checkDenyOperation(
|
|
86
|
-
const { filters, roles } = rules[collName] || {}
|
|
87
|
-
|
|
138
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
|
|
88
139
|
// Apply access control filters
|
|
89
140
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
90
141
|
|
|
@@ -92,6 +143,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
92
143
|
const result = await collection.findOne({ $and: formattedQuery })
|
|
93
144
|
const winningRole = getWinningRole(result, user, roles)
|
|
94
145
|
|
|
146
|
+
logDebug('delete winningRole', {
|
|
147
|
+
collection: collName,
|
|
148
|
+
userId: getUserId(user),
|
|
149
|
+
winningRoleName: winningRole?.name ?? null
|
|
150
|
+
})
|
|
95
151
|
const { status } = winningRole
|
|
96
152
|
? await checkValidation(
|
|
97
153
|
winningRole,
|
|
@@ -103,7 +159,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
103
159
|
},
|
|
104
160
|
user
|
|
105
161
|
)
|
|
106
|
-
:
|
|
162
|
+
: fallbackAccess(result)
|
|
107
163
|
|
|
108
164
|
if (!status) {
|
|
109
165
|
throw new Error('Delete not permitted')
|
|
@@ -134,10 +190,8 @@ const getOperators: GetOperatorsFunction = (
|
|
|
134
190
|
* This ensures that only users with the correct permissions can insert data into the collection.
|
|
135
191
|
*/
|
|
136
192
|
insertOne: async (data, options) => {
|
|
137
|
-
const { roles } = rules[collName] || {}
|
|
138
|
-
|
|
139
193
|
if (!run_as_system) {
|
|
140
|
-
checkDenyOperation(
|
|
194
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
|
|
141
195
|
const winningRole = getWinningRole(data, user, roles)
|
|
142
196
|
|
|
143
197
|
const { status, document } = winningRole
|
|
@@ -151,12 +205,19 @@ const getOperators: GetOperatorsFunction = (
|
|
|
151
205
|
},
|
|
152
206
|
user
|
|
153
207
|
)
|
|
154
|
-
:
|
|
208
|
+
: fallbackAccess(data)
|
|
155
209
|
|
|
156
210
|
if (!status || !isEqual(data, document)) {
|
|
157
211
|
throw new Error('Insert not permitted')
|
|
158
212
|
}
|
|
159
|
-
|
|
213
|
+
logService('insertOne payload', { collName, data })
|
|
214
|
+
const insertResult = await collection.insertOne(data, options)
|
|
215
|
+
logService('insertOne result', {
|
|
216
|
+
collName,
|
|
217
|
+
insertedId: insertResult.insertedId.toString(),
|
|
218
|
+
document: data
|
|
219
|
+
})
|
|
220
|
+
return insertResult
|
|
160
221
|
}
|
|
161
222
|
// System mode: insert without validation
|
|
162
223
|
return collection.insertOne(data, options)
|
|
@@ -185,8 +246,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
185
246
|
updateOne: async (query, data, options) => {
|
|
186
247
|
if (!run_as_system) {
|
|
187
248
|
|
|
188
|
-
checkDenyOperation(
|
|
189
|
-
const { filters, roles } = rules[collName] || {}
|
|
249
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
190
250
|
// Apply access control filters
|
|
191
251
|
|
|
192
252
|
// Normalize _id
|
|
@@ -210,10 +270,9 @@ const getOperators: GetOperatorsFunction = (
|
|
|
210
270
|
// const docToCheck = hasOperators
|
|
211
271
|
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
212
272
|
// : data
|
|
213
|
-
const [matchQuery] = formattedQuery; // TODO da chiedere/capire perchè è solo uno. tutti gli altri { $match: { $and: formattedQuery } }
|
|
214
273
|
const pipeline = [
|
|
215
274
|
{
|
|
216
|
-
$match:
|
|
275
|
+
$match: { $and: safeQuery }
|
|
217
276
|
},
|
|
218
277
|
{
|
|
219
278
|
$limit: 1
|
|
@@ -235,14 +294,14 @@ const getOperators: GetOperatorsFunction = (
|
|
|
235
294
|
},
|
|
236
295
|
user
|
|
237
296
|
)
|
|
238
|
-
:
|
|
297
|
+
: fallbackAccess(docToCheck)
|
|
239
298
|
// Ensure no unauthorized changes are made
|
|
240
299
|
const areDocumentsEqual = isEqual(document, docToCheck)
|
|
241
300
|
|
|
242
301
|
if (!status || !areDocumentsEqual) {
|
|
243
302
|
throw new Error('Update not permitted')
|
|
244
303
|
}
|
|
245
|
-
return collection.updateOne({ $and:
|
|
304
|
+
return collection.updateOne({ $and: safeQuery }, data, options)
|
|
246
305
|
}
|
|
247
306
|
return collection.updateOne(query, data, options)
|
|
248
307
|
},
|
|
@@ -265,32 +324,32 @@ const getOperators: GetOperatorsFunction = (
|
|
|
265
324
|
*/
|
|
266
325
|
find: (query) => {
|
|
267
326
|
if (!run_as_system) {
|
|
268
|
-
checkDenyOperation(
|
|
269
|
-
const { filters, roles } = rules[collName] || {}
|
|
270
|
-
|
|
327
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
271
328
|
// Pre-query filtering based on access control rules
|
|
272
329
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
273
330
|
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
|
|
274
331
|
// 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)
|
|
332
|
+
const cursor = collection.find(currentQuery)
|
|
333
|
+
const originalToArray = cursor.toArray.bind(cursor)
|
|
281
334
|
|
|
282
335
|
/**
|
|
283
336
|
* Overridden `toArray` method that validates each document for read access.
|
|
284
337
|
*
|
|
285
338
|
* @returns {Promise<Document[]>} An array of documents the user is authorized to read.
|
|
286
339
|
*/
|
|
287
|
-
|
|
288
|
-
const response = await
|
|
340
|
+
cursor.toArray = async () => {
|
|
341
|
+
const response = await originalToArray()
|
|
289
342
|
|
|
290
343
|
const filteredResponse = await Promise.all(
|
|
291
344
|
response.map(async (currentDoc) => {
|
|
292
345
|
const winningRole = getWinningRole(currentDoc, user, roles)
|
|
293
346
|
|
|
347
|
+
logDebug('find winningRole', {
|
|
348
|
+
collection: collName,
|
|
349
|
+
userId: getUserId(user),
|
|
350
|
+
winningRoleName: winningRole?.name ?? null,
|
|
351
|
+
rolesLength: roles.length
|
|
352
|
+
})
|
|
294
353
|
const { status, document } = winningRole
|
|
295
354
|
? await checkValidation(
|
|
296
355
|
winningRole,
|
|
@@ -302,16 +361,16 @@ const getOperators: GetOperatorsFunction = (
|
|
|
302
361
|
},
|
|
303
362
|
user
|
|
304
363
|
)
|
|
305
|
-
:
|
|
364
|
+
: fallbackAccess(currentDoc)
|
|
306
365
|
|
|
307
366
|
return status ? document : undefined
|
|
308
367
|
})
|
|
309
368
|
)
|
|
310
369
|
|
|
311
|
-
return filteredResponse.filter(Boolean)
|
|
370
|
+
return filteredResponse.filter(Boolean) as WithId<Document>[]
|
|
312
371
|
}
|
|
313
372
|
|
|
314
|
-
return
|
|
373
|
+
return cursor
|
|
315
374
|
}
|
|
316
375
|
// System mode: return original unfiltered cursor
|
|
317
376
|
return collection.find(query)
|
|
@@ -337,9 +396,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
337
396
|
*/
|
|
338
397
|
watch: (pipeline = [], options) => {
|
|
339
398
|
if (!run_as_system) {
|
|
340
|
-
checkDenyOperation(
|
|
341
|
-
const { filters, roles } = rules[collName] || {}
|
|
342
|
-
|
|
399
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
343
400
|
// Apply access filters to initial change stream pipeline
|
|
344
401
|
const formattedQuery = getFormattedQuery(filters, {}, user)
|
|
345
402
|
|
|
@@ -377,7 +434,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
377
434
|
},
|
|
378
435
|
user
|
|
379
436
|
)
|
|
380
|
-
:
|
|
437
|
+
: fallbackAccess(fullDocument)
|
|
381
438
|
|
|
382
439
|
const { status: updatedFieldsStatus, document: updatedFields } = winningRole
|
|
383
440
|
? await checkValidation(
|
|
@@ -390,7 +447,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
390
447
|
},
|
|
391
448
|
user
|
|
392
449
|
)
|
|
393
|
-
:
|
|
450
|
+
: fallbackAccess(updateDescription?.updatedFields)
|
|
394
451
|
|
|
395
452
|
return { status, document, updatedFieldsStatus, updatedFields }
|
|
396
453
|
}
|
|
@@ -425,52 +482,48 @@ const getOperators: GetOperatorsFunction = (
|
|
|
425
482
|
},
|
|
426
483
|
//TODO -> add filter & rules in aggregate
|
|
427
484
|
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
485
|
if (run_as_system || !isClient) {
|
|
432
486
|
return collection.aggregate(pipeline, options)
|
|
433
487
|
}
|
|
434
|
-
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
435
488
|
|
|
436
|
-
|
|
489
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
490
|
+
|
|
491
|
+
const rulesConfig = collectionRules ?? { filters, roles }
|
|
492
|
+
|
|
493
|
+
ensureClientPipelineStages(pipeline)
|
|
494
|
+
|
|
437
495
|
const formattedQuery = getFormattedQuery(filters, {}, user)
|
|
496
|
+
logDebug('aggregate formattedQuery', {
|
|
497
|
+
collection: collName,
|
|
498
|
+
formattedQuery,
|
|
499
|
+
pipeline
|
|
500
|
+
})
|
|
438
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
|
+
})
|
|
439
515
|
|
|
440
516
|
const guardedPipeline = [
|
|
517
|
+
...(hiddenFields.length ? [{ $unset: hiddenFields }] : []),
|
|
441
518
|
...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
|
|
442
519
|
...(projection ? [{ $project: projection }] : []),
|
|
443
|
-
...
|
|
520
|
+
...sanitizedPipeline
|
|
444
521
|
]
|
|
445
522
|
|
|
446
|
-
// const pipelineCollections = getCollectionsFromPipeline(pipeline)
|
|
447
|
-
|
|
448
|
-
// console.log(pipelineCollections)
|
|
449
|
-
|
|
450
|
-
// pipelineCollections.every((collection) => checkDenyOperation(rules, collection, CRUD_OPERATIONS.READ))
|
|
451
|
-
|
|
452
523
|
const originalCursor = collection.aggregate(guardedPipeline, options)
|
|
453
524
|
const newCursor = Object.create(originalCursor)
|
|
454
525
|
|
|
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
|
-
}
|
|
526
|
+
newCursor.toArray = async () => originalCursor.toArray()
|
|
474
527
|
|
|
475
528
|
return newCursor
|
|
476
529
|
},
|
|
@@ -493,8 +546,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
493
546
|
*/
|
|
494
547
|
insertMany: async (documents, options) => {
|
|
495
548
|
if (!run_as_system) {
|
|
496
|
-
checkDenyOperation(
|
|
497
|
-
const { roles } = rules[collName] || {}
|
|
549
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.CREATE)
|
|
498
550
|
// Validate each document against user's roles
|
|
499
551
|
const filteredItems = await Promise.all(
|
|
500
552
|
documents.map(async (currentDoc) => {
|
|
@@ -511,7 +563,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
511
563
|
},
|
|
512
564
|
user
|
|
513
565
|
)
|
|
514
|
-
|
|
566
|
+
: fallbackAccess(currentDoc)
|
|
515
567
|
|
|
516
568
|
return status ? document : undefined
|
|
517
569
|
})
|
|
@@ -530,8 +582,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
530
582
|
},
|
|
531
583
|
updateMany: async (query, data, options) => {
|
|
532
584
|
if (!run_as_system) {
|
|
533
|
-
checkDenyOperation(
|
|
534
|
-
const { filters, roles } = rules[collName] || {}
|
|
585
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
535
586
|
// Apply access control filters
|
|
536
587
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
537
588
|
|
|
@@ -576,7 +627,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
576
627
|
},
|
|
577
628
|
user
|
|
578
629
|
)
|
|
579
|
-
|
|
630
|
+
: fallbackAccess(currentDoc)
|
|
580
631
|
|
|
581
632
|
return status ? document : undefined
|
|
582
633
|
})
|
|
@@ -611,9 +662,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
611
662
|
*/
|
|
612
663
|
deleteMany: async (query = {}) => {
|
|
613
664
|
if (!run_as_system) {
|
|
614
|
-
checkDenyOperation(
|
|
615
|
-
const { filters, roles } = rules[collName] || {}
|
|
616
|
-
|
|
665
|
+
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.DELETE)
|
|
617
666
|
// Apply access control filters
|
|
618
667
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
619
668
|
|
|
@@ -636,7 +685,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
636
685
|
},
|
|
637
686
|
user
|
|
638
687
|
)
|
|
639
|
-
:
|
|
688
|
+
: fallbackAccess(currentDoc)
|
|
640
689
|
|
|
641
690
|
return status ? document : undefined
|
|
642
691
|
})
|
|
@@ -662,7 +711,8 @@ const getOperators: GetOperatorsFunction = (
|
|
|
662
711
|
// If running as system, bypass access control and delete directly
|
|
663
712
|
return collection.deleteMany(query)
|
|
664
713
|
}
|
|
665
|
-
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
666
716
|
|
|
667
717
|
const MongodbAtlas: MongodbAtlasFunction = (
|
|
668
718
|
app,
|
|
@@ -671,9 +721,12 @@ const MongodbAtlas: MongodbAtlasFunction = (
|
|
|
671
721
|
db: (dbName: string) => {
|
|
672
722
|
return {
|
|
673
723
|
collection: (collName: string) => {
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
724
|
+
const mongoClient = app.mongo.client as unknown as {
|
|
725
|
+
db: (database: string) => {
|
|
726
|
+
collection: (name: string) => Collection<Document>
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const collection: Collection<Document> = mongoClient.db(dbName).collection(collName)
|
|
677
730
|
return getOperators(collection, {
|
|
678
731
|
rules,
|
|
679
732
|
collName,
|