@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.
Files changed (49) hide show
  1. package/dist/auth/controller.js +3 -3
  2. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  3. package/dist/auth/providers/local-userpass/controller.js +37 -4
  4. package/dist/auth/utils.d.ts +1 -0
  5. package/dist/auth/utils.d.ts.map +1 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +3 -2
  9. package/dist/features/endpoints/index.d.ts +1 -1
  10. package/dist/features/endpoints/index.d.ts.map +1 -1
  11. package/dist/features/endpoints/index.js +3 -3
  12. package/dist/features/endpoints/interface.d.ts +4 -0
  13. package/dist/features/endpoints/interface.d.ts.map +1 -1
  14. package/dist/features/endpoints/utils.d.ts +1 -1
  15. package/dist/features/endpoints/utils.d.ts.map +1 -1
  16. package/dist/features/endpoints/utils.js +18 -10
  17. package/dist/features/functions/utils.d.ts +2 -2
  18. package/dist/features/functions/utils.js +3 -1
  19. package/dist/features/rules/interface.d.ts +42 -0
  20. package/dist/features/rules/interface.d.ts.map +1 -1
  21. package/dist/features/rules/interface.js +7 -0
  22. package/dist/features/rules/utils.js +41 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +39 -1
  25. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  26. package/dist/services/mongodb-atlas/index.js +45 -2
  27. package/dist/services/mongodb-atlas/model.d.ts +7 -1
  28. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  29. package/dist/services/mongodb-atlas/model.js +8 -0
  30. package/dist/services/mongodb-atlas/utils.d.ts +9 -2
  31. package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
  32. package/dist/services/mongodb-atlas/utils.js +104 -1
  33. package/dist/utils/rules.js +2 -1
  34. package/package.json +3 -1
  35. package/src/auth/controller.ts +3 -3
  36. package/src/auth/providers/local-userpass/controller.ts +41 -4
  37. package/src/auth/utils.ts +1 -0
  38. package/src/constants.ts +4 -2
  39. package/src/features/endpoints/index.ts +4 -3
  40. package/src/features/endpoints/interface.ts +4 -0
  41. package/src/features/endpoints/utils.ts +28 -11
  42. package/src/features/functions/utils.ts +3 -3
  43. package/src/features/rules/interface.ts +35 -1
  44. package/src/features/rules/utils.ts +46 -0
  45. package/src/index.ts +20 -1
  46. package/src/services/mongodb-atlas/index.ts +60 -5
  47. package/src/services/mongodb-atlas/model.ts +10 -1
  48. package/src/services/mongodb-atlas/utils.ts +129 -2
  49. 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) => collection.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, { rules, collName, user, run_as_system })
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
+ };
@@ -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
- typeof value === 'string' ? `"${value}"` : value && JSON.stringify(value)
17
- return `:${value !== undefined ? finalValue : match}` // Sostituisci se esiste, altrimenti lascia il placeholder
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(