@flowerforce/flowerbase 1.0.2 → 1.0.3-beta.0
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.js +3 -3
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +37 -4
- package/dist/auth/utils.d.ts +1 -0
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -2
- package/dist/features/endpoints/index.d.ts +1 -1
- package/dist/features/endpoints/index.d.ts.map +1 -1
- package/dist/features/endpoints/index.js +3 -3
- package/dist/features/endpoints/interface.d.ts +4 -0
- package/dist/features/endpoints/interface.d.ts.map +1 -1
- package/dist/features/endpoints/utils.d.ts +1 -1
- package/dist/features/endpoints/utils.d.ts.map +1 -1
- package/dist/features/endpoints/utils.js +18 -10
- package/dist/features/functions/utils.d.ts +2 -2
- package/dist/features/functions/utils.js +3 -1
- package/dist/features/rules/interface.d.ts +42 -0
- package/dist/features/rules/interface.d.ts.map +1 -1
- package/dist/features/rules/interface.js +7 -0
- package/dist/features/rules/utils.js +41 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -1
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +45 -2
- package/dist/services/mongodb-atlas/model.d.ts +7 -1
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/model.js +8 -0
- 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 +104 -1
- package/dist/utils/rules.js +2 -1
- package/package.json +3 -1
- package/src/auth/controller.ts +3 -3
- package/src/auth/providers/local-userpass/controller.ts +41 -4
- package/src/auth/utils.ts +1 -0
- package/src/constants.ts +4 -2
- package/src/features/endpoints/index.ts +4 -3
- package/src/features/endpoints/interface.ts +4 -0
- package/src/features/endpoints/utils.ts +28 -11
- package/src/features/functions/utils.ts +3 -3
- package/src/features/rules/interface.ts +35 -1
- package/src/features/rules/utils.ts +46 -0
- package/src/index.ts +20 -1
- package/src/services/mongodb-atlas/index.ts +60 -5
- package/src/services/mongodb-atlas/model.ts +10 -1
- package/src/services/mongodb-atlas/utils.ts +129 -2
- package/src/utils/rules.ts +3 -3
|
@@ -3,8 +3,8 @@ import isEqual from 'lodash/isEqual'
|
|
|
3
3
|
import { Collection, Document, EventsDescription, FindCursor, WithId } from 'mongodb'
|
|
4
4
|
import { checkValidation } from '../../utils/roles/machines'
|
|
5
5
|
import { getWinningRole } from '../../utils/roles/machines/utils'
|
|
6
|
-
import { GetOperatorsFunction, MongodbAtlasFunction } from './model'
|
|
7
|
-
import { getFormattedQuery } from './utils'
|
|
6
|
+
import { CRUD_OPERATIONS, GetOperatorsFunction, MongodbAtlasFunction } from './model'
|
|
7
|
+
import { applyAccessControlToPipeline, checkDenyOperation, getFormattedProjection, getFormattedQuery } from './utils'
|
|
8
8
|
|
|
9
9
|
//TODO aggiungere no-sql inject security
|
|
10
10
|
const getOperators: GetOperatorsFunction = (
|
|
@@ -28,6 +28,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
28
28
|
*/
|
|
29
29
|
findOne: async (query) => {
|
|
30
30
|
if (!run_as_system) {
|
|
31
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
31
32
|
const { filters, roles } = rules[collName] || {}
|
|
32
33
|
|
|
33
34
|
// Apply access control filters to the query
|
|
@@ -75,6 +76,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
75
76
|
*/
|
|
76
77
|
deleteOne: async (query = {}) => {
|
|
77
78
|
if (!run_as_system) {
|
|
79
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.DELETE)
|
|
78
80
|
const { filters, roles } = rules[collName] || {}
|
|
79
81
|
|
|
80
82
|
// Apply access control filters
|
|
@@ -129,6 +131,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
129
131
|
const { roles } = rules[collName] || {}
|
|
130
132
|
|
|
131
133
|
if (!run_as_system) {
|
|
134
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.CREATE)
|
|
132
135
|
const winningRole = getWinningRole(data, user, roles)
|
|
133
136
|
|
|
134
137
|
const { status, document } = winningRole
|
|
@@ -175,6 +178,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
175
178
|
*/
|
|
176
179
|
updateOne: async (query, data, options) => {
|
|
177
180
|
if (!run_as_system) {
|
|
181
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
178
182
|
const { filters, roles } = rules[collName] || {}
|
|
179
183
|
// Apply access control filters
|
|
180
184
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
@@ -252,12 +256,13 @@ const getOperators: GetOperatorsFunction = (
|
|
|
252
256
|
*/
|
|
253
257
|
find: (query) => {
|
|
254
258
|
if (!run_as_system) {
|
|
259
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
255
260
|
const { filters, roles } = rules[collName] || {}
|
|
256
261
|
|
|
257
262
|
// Pre-query filtering based on access control rules
|
|
258
263
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
264
|
+
// aggiunto filter per evitare questo errore: $and argument's entries must be objects
|
|
259
265
|
const originalCursor = collection.find({ $and: formattedQuery })
|
|
260
|
-
|
|
261
266
|
// Clone the cursor to override `toArray` with post-query validation
|
|
262
267
|
const client = originalCursor[
|
|
263
268
|
'client' as keyof typeof originalCursor
|
|
@@ -322,6 +327,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
322
327
|
*/
|
|
323
328
|
watch: (pipeline = [], options) => {
|
|
324
329
|
if (!run_as_system) {
|
|
330
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
325
331
|
const { filters, roles } = rules[collName] || {}
|
|
326
332
|
|
|
327
333
|
// Apply access filters to initial change stream pipeline
|
|
@@ -406,7 +412,51 @@ const getOperators: GetOperatorsFunction = (
|
|
|
406
412
|
return collection.watch(pipeline, options)
|
|
407
413
|
},
|
|
408
414
|
//TODO -> add filter & rules in aggregate
|
|
409
|
-
aggregate: (pipeline, options) =>
|
|
415
|
+
aggregate: async (pipeline = [], options) => {
|
|
416
|
+
|
|
417
|
+
if (run_as_system) {
|
|
418
|
+
return collection.aggregate(pipeline, options);
|
|
419
|
+
}
|
|
420
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.READ)
|
|
421
|
+
|
|
422
|
+
const { filters = [], roles = [] } = rules[collection.collectionName] || {};
|
|
423
|
+
const formattedQuery = getFormattedQuery(filters, {}, user);
|
|
424
|
+
const projection = getFormattedProjection(filters);
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
const guardedPipeline = [
|
|
428
|
+
...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
|
|
429
|
+
...(projection ? [{ $project: projection }] : []),
|
|
430
|
+
...applyAccessControlToPipeline(pipeline, rules, user)
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
// const pipelineCollections = getCollectionsFromPipeline(pipeline)
|
|
434
|
+
|
|
435
|
+
// console.log(pipelineCollections)
|
|
436
|
+
|
|
437
|
+
// pipelineCollections.every((collection) => checkDenyOperation(rules, collection, CRUD_OPERATIONS.READ))
|
|
438
|
+
|
|
439
|
+
const originalCursor = collection.aggregate(guardedPipeline, options);
|
|
440
|
+
const newCursor = Object.create(originalCursor);
|
|
441
|
+
|
|
442
|
+
newCursor.toArray = async () => {
|
|
443
|
+
const results = await originalCursor.toArray();
|
|
444
|
+
|
|
445
|
+
const filtered = await Promise.all(
|
|
446
|
+
results.map(async (doc) => {
|
|
447
|
+
const role = getWinningRole(doc, user, roles);
|
|
448
|
+
const { status, document } = role
|
|
449
|
+
? await checkValidation(role, { type: 'read', roles, cursor: doc, expansions: {} }, user)
|
|
450
|
+
: { status: !roles?.length, document: doc };
|
|
451
|
+
return status ? document : undefined;
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
return filtered.filter(Boolean);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
return newCursor;
|
|
459
|
+
},
|
|
410
460
|
/**
|
|
411
461
|
* Inserts multiple documents into a MongoDB collection with optional role-based access control and validation.
|
|
412
462
|
*
|
|
@@ -426,6 +476,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
426
476
|
*/
|
|
427
477
|
insertMany: async (documents, options) => {
|
|
428
478
|
if (!run_as_system) {
|
|
479
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.CREATE)
|
|
429
480
|
const { roles } = rules[collName] || {}
|
|
430
481
|
// Validate each document against user's roles
|
|
431
482
|
const filteredItems = await Promise.all(
|
|
@@ -462,6 +513,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
462
513
|
},
|
|
463
514
|
updateMany: async (query, data, options) => {
|
|
464
515
|
if (!run_as_system) {
|
|
516
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
465
517
|
const { filters, roles } = rules[collName] || {}
|
|
466
518
|
// Apply access control filters
|
|
467
519
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
@@ -539,6 +591,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
539
591
|
*/
|
|
540
592
|
deleteMany: async (query = {}) => {
|
|
541
593
|
if (!run_as_system) {
|
|
594
|
+
checkDenyOperation(rules, collection.collectionName, CRUD_OPERATIONS.DELETE)
|
|
542
595
|
const { filters, roles } = rules[collName] || {}
|
|
543
596
|
|
|
544
597
|
// Apply access control filters
|
|
@@ -601,7 +654,9 @@ const MongodbAtlas: MongodbAtlasFunction = (
|
|
|
601
654
|
const collection: Collection<Document> = app.mongo.client
|
|
602
655
|
.db(dbName)
|
|
603
656
|
.collection(collName)
|
|
604
|
-
return getOperators(collection, {
|
|
657
|
+
return getOperators(collection, {
|
|
658
|
+
rules, collName, user, run_as_system
|
|
659
|
+
})
|
|
605
660
|
}
|
|
606
661
|
}
|
|
607
662
|
}
|
|
@@ -54,7 +54,7 @@ export type GetOperatorsFunction = (
|
|
|
54
54
|
watch: (...params: Parameters<Method<'watch'>>) => ReturnType<Method<'watch'>>
|
|
55
55
|
aggregate: (
|
|
56
56
|
...params: Parameters<Method<'aggregate'>>
|
|
57
|
-
) => ReturnType<Method<'aggregate'
|
|
57
|
+
) => Promise<ReturnType<Method<'aggregate'>>>
|
|
58
58
|
insertMany: (
|
|
59
59
|
...params: Parameters<Method<'insertMany'>>
|
|
60
60
|
) => ReturnType<Method<'insertMany'>>
|
|
@@ -65,3 +65,12 @@ export type GetOperatorsFunction = (
|
|
|
65
65
|
...params: Parameters<Method<'deleteMany'>>
|
|
66
66
|
) => ReturnType<Method<'deleteMany'>>
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
export enum CRUD_OPERATIONS {
|
|
71
|
+
CREATE = "CREATE",
|
|
72
|
+
READ = "READ",
|
|
73
|
+
UPDATE = "UPDATE",
|
|
74
|
+
DELETE = "DELETE"
|
|
75
|
+
|
|
76
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Collection, Document } from 'mongodb'
|
|
2
2
|
import { User } from '../../auth/dtos'
|
|
3
|
-
import { Filter } from '../../features/rules/interface'
|
|
3
|
+
import { AggregationPipeline, AggregationPipelineStage, Filter, LookupStage, Projection, Rules, STAGES_TO_SEARCH, UnionWithStage } from '../../features/rules/interface'
|
|
4
4
|
import { Role } from '../../utils/roles/interface'
|
|
5
5
|
import { expandQuery } from '../../utils/rules'
|
|
6
6
|
import rulesMatcherUtils from '../../utils/rules-matcher/utils'
|
|
7
|
-
import { GetValidRuleParams } from './model'
|
|
7
|
+
import { CRUD_OPERATIONS, GetValidRuleParams } from './model'
|
|
8
8
|
|
|
9
9
|
export const getValidRule = <T extends Role | Filter>({
|
|
10
10
|
filters = [],
|
|
@@ -44,3 +44,130 @@ export const getFormattedQuery = (
|
|
|
44
44
|
query
|
|
45
45
|
].filter(Boolean)
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
export const getFormattedProjection = (filters: Filter[] = [], user?: User): Projection | null => {
|
|
49
|
+
const projections = filters.filter((filter) => {
|
|
50
|
+
if (filter.projection) {
|
|
51
|
+
const preFilter = getValidRule({ filters, user })
|
|
52
|
+
const isValidPreFilter = !!preFilter?.length
|
|
53
|
+
return isValidPreFilter
|
|
54
|
+
}
|
|
55
|
+
return false
|
|
56
|
+
}).map(f => f.projection)
|
|
57
|
+
if (!projections.length) return null;
|
|
58
|
+
return Object.assign({}, ...projections);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
export const applyAccessControlToPipeline = (
|
|
63
|
+
pipeline: AggregationPipeline,
|
|
64
|
+
rules: Record<string, {
|
|
65
|
+
filters?: Filter[];
|
|
66
|
+
roles?: Role[];
|
|
67
|
+
}>,
|
|
68
|
+
user: User
|
|
69
|
+
): AggregationPipeline => {
|
|
70
|
+
return pipeline.map((stage) => {
|
|
71
|
+
const [stageName] = Object.keys(stage);
|
|
72
|
+
const value = stage[stageName as keyof typeof stage];
|
|
73
|
+
|
|
74
|
+
// CASE LOOKUP
|
|
75
|
+
if (stageName === STAGES_TO_SEARCH.LOOKUP) {
|
|
76
|
+
const lookUpStage = value as LookupStage
|
|
77
|
+
const currentCollection = lookUpStage.from
|
|
78
|
+
const lookupRules = rules[currentCollection] || {};
|
|
79
|
+
const formattedQuery = getFormattedQuery(lookupRules.filters, {}, user);
|
|
80
|
+
const projection = getFormattedProjection(lookupRules.filters);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
$lookup: {
|
|
84
|
+
...lookUpStage,
|
|
85
|
+
pipeline: [
|
|
86
|
+
...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
|
|
87
|
+
...(projection ? [{ $project: projection }] : []),
|
|
88
|
+
...applyAccessControlToPipeline(lookUpStage.pipeline || [], rules, user)
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// CASE LOOKUP
|
|
95
|
+
if (stageName === STAGES_TO_SEARCH.UNION_WITH) {
|
|
96
|
+
const unionWithStage = value as UnionWithStage
|
|
97
|
+
const isSimpleStage = typeof unionWithStage === "string"
|
|
98
|
+
const currentCollection = isSimpleStage ? unionWithStage : unionWithStage.coll;
|
|
99
|
+
const unionRules = rules[currentCollection] || {};
|
|
100
|
+
const formattedQuery = getFormattedQuery(unionRules.filters, {}, user);
|
|
101
|
+
const projection = getFormattedProjection(unionRules.filters);
|
|
102
|
+
|
|
103
|
+
const nestedPipeline = isSimpleStage ? [] : (unionWithStage.pipeline || [])
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
$unionWith: {
|
|
107
|
+
coll: currentCollection,
|
|
108
|
+
pipeline: [
|
|
109
|
+
...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
|
|
110
|
+
...(projection ? [{ $project: projection }] : []),
|
|
111
|
+
...applyAccessControlToPipeline((nestedPipeline), rules, user)
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// CASE FACET
|
|
118
|
+
if (stageName === STAGES_TO_SEARCH.FACET) {
|
|
119
|
+
const modifiedFacets = Object.fromEntries(
|
|
120
|
+
(Object.entries(value) as [string, AggregationPipelineStage[]][]).map(([facetKey, facetPipeline]) => {
|
|
121
|
+
return [
|
|
122
|
+
facetKey,
|
|
123
|
+
applyAccessControlToPipeline(facetPipeline, rules, user)
|
|
124
|
+
];
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return { $facet: modifiedFacets };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return stage;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const checkDenyOperation = (rules: Rules, collectionName: string, operation: CRUD_OPERATIONS) => {
|
|
136
|
+
const collectionRules = rules[collectionName]
|
|
137
|
+
if (!collectionRules) {
|
|
138
|
+
throw new Error(`${operation} FORBIDDEN!`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export const getCollectionsFromPipeline = (pipeline: Document[]) => {
|
|
142
|
+
return pipeline.reduce<string[]>((acc, stage) => {
|
|
143
|
+
const [stageKey] = Object.keys(stage);
|
|
144
|
+
const stageValue = stage[stageKey];
|
|
145
|
+
const subPipeline = stageValue?.pipeline;
|
|
146
|
+
|
|
147
|
+
if (stageKey === STAGES_TO_SEARCH.LOOKUP) {
|
|
148
|
+
acc.push(...[stageValue.from, ...acc]);
|
|
149
|
+
if (subPipeline) {
|
|
150
|
+
const collections = getCollectionsFromPipeline(subPipeline);
|
|
151
|
+
acc.push(...[stageValue.from, ...collections]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (stageKey === STAGES_TO_SEARCH.FACET) {
|
|
156
|
+
for (const sub of Object.values(stageValue) as Document[][]) {
|
|
157
|
+
const collections = getCollectionsFromPipeline(sub);
|
|
158
|
+
acc.push(...collections);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
stageKey === STAGES_TO_SEARCH.UNION_WITH &&
|
|
164
|
+
typeof stageValue === 'object' &&
|
|
165
|
+
subPipeline
|
|
166
|
+
) {
|
|
167
|
+
const collections = getCollectionsFromPipeline(subPipeline);
|
|
168
|
+
acc.push(...[stageValue.coll, ...collections]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return acc;
|
|
172
|
+
}, []);
|
|
173
|
+
};
|
package/src/utils/rules.ts
CHANGED
|
@@ -12,9 +12,9 @@ export function expandQuery(
|
|
|
12
12
|
|
|
13
13
|
const callback = (match: string, path: string) => {
|
|
14
14
|
const value = get(objs, `%%${path}`) // Recupera il valore annidato da values
|
|
15
|
-
const finalValue =
|
|
16
|
-
|
|
17
|
-
return
|
|
15
|
+
const finalValue = typeof value === 'string' ? `"${value}"` : value && JSON.stringify(value)
|
|
16
|
+
// TODO tolto i primi : creava questo tipo di oggetto {"userId"::"%%user.id"}
|
|
17
|
+
return value !== undefined ? finalValue : match // Sostituisci se esiste, altrimenti lascia il placeholder
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
expandedQuery = expandedQuery.replace(
|