@flowerforce/flowerbase 1.8.3 → 1.8.4-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.
@@ -1,7 +1,48 @@
1
1
  import { evaluateExpression } from '../roles/helpers'
2
2
  import { Params } from '../roles/interface'
3
+ import { GenerateContext } from '../context'
4
+ import { StateManager } from '../../state'
5
+
6
+ jest.mock('../context', () => ({
7
+ GenerateContext: jest.fn()
8
+ }))
9
+
10
+ jest.mock('../../state', () => ({
11
+ StateManager: {
12
+ select: jest.fn()
13
+ }
14
+ }))
15
+
16
+ jest.mock('../../services', () => ({
17
+ services: {}
18
+ }))
19
+
20
+ const mockedGenerateContext = jest.mocked(GenerateContext)
21
+ const mockedSelect = jest.mocked(StateManager.select)
3
22
 
4
23
  describe('evaluateExpression', () => {
24
+ beforeEach(() => {
25
+ jest.clearAllMocks()
26
+ mockedSelect.mockImplementation(((key: string) => {
27
+ switch (key) {
28
+ case 'functions':
29
+ return {
30
+ checkAccess: {
31
+ name: 'checkAccess'
32
+ }
33
+ }
34
+ case 'app':
35
+ return {
36
+ id: 'app-id'
37
+ }
38
+ case 'rules':
39
+ return {}
40
+ default:
41
+ return undefined
42
+ }
43
+ }) as never)
44
+ })
45
+
5
46
  it('supports insert-only write expressions that rely on %%prevRoot', async () => {
6
47
  const expression = {
7
48
  '%%prevRoot': {
@@ -30,4 +71,52 @@ describe('evaluateExpression', () => {
30
71
  await expect(evaluateExpression(insertParams, expression)).resolves.toBe(true)
31
72
  await expect(evaluateExpression(readParams, expression)).resolves.toBe(false)
32
73
  })
74
+
75
+ it('supports nested %function conditions inside %or/%and expressions', async () => {
76
+ mockedGenerateContext.mockResolvedValue(true)
77
+
78
+ const expression = {
79
+ '%or': [
80
+ {
81
+ '%%root.company': 'company-1'
82
+ },
83
+ {
84
+ '%and': [
85
+ {
86
+ '%%user.custom_data.type': 'customer'
87
+ },
88
+ {
89
+ '%%true': {
90
+ '%function': {
91
+ name: 'checkAccess',
92
+ arguments: ['%%root.company']
93
+ }
94
+ }
95
+ }
96
+ ]
97
+ }
98
+ ]
99
+ }
100
+
101
+ const params = {
102
+ type: 'read',
103
+ cursor: { company: 'company-2' },
104
+ expansions: {},
105
+ roles: []
106
+ } as Params
107
+
108
+ const user = {
109
+ custom_data: {
110
+ type: 'customer'
111
+ }
112
+ }
113
+
114
+ await expect(evaluateExpression(params, expression, user as never)).resolves.toBe(true)
115
+ expect(mockedGenerateContext).toHaveBeenCalledWith(
116
+ expect.objectContaining({
117
+ args: ['company-2'],
118
+ functionName: 'checkAccess'
119
+ })
120
+ )
121
+ })
33
122
  })
@@ -70,4 +70,21 @@ describe('getWinningRole', () => {
70
70
  const result = getWinningRole(null, mockUser, mockRoles)
71
71
  expect(result).toEqual(mockRoles[0])
72
72
  })
73
+
74
+ it('should return the first matching role asynchronously', async () => {
75
+ const mockCheckApplyWhenAsync = jest
76
+ .spyOn(Utils, 'checkApplyWhenAsync')
77
+ .mockResolvedValueOnce(true)
78
+
79
+ await expect(Utils.getWinningRoleAsync(mockDocument, mockUser, mockRoles)).resolves.toEqual(
80
+ mockRoles[0]
81
+ )
82
+ expect(mockCheckApplyWhenAsync).toHaveBeenCalledWith(
83
+ mockRoles[0].apply_when,
84
+ mockUser,
85
+ mockDocument
86
+ )
87
+ expect(mockCheckApplyWhenAsync).toHaveBeenCalledTimes(1)
88
+ mockCheckApplyWhenAsync.mockReset()
89
+ })
73
90
  })
@@ -7,6 +7,8 @@ import { PermissionExpression } from './interface'
7
7
  import { MachineContext } from './machines/interface'
8
8
 
9
9
  const functionsConditions = ['%%true', '%%false']
10
+ const andConditions = ['$and', '%and']
11
+ const orConditions = ['$or', '%or']
10
12
 
11
13
  const normalizeUserRole = (user?: MachineContext['user']) => {
12
14
  if (!user) return user
@@ -22,30 +24,121 @@ const normalizeUserRole = (user?: MachineContext['user']) => {
22
24
  : user
23
25
  }
24
26
 
25
- export const evaluateExpression = async (
27
+ const buildEvaluationContext = (
26
28
  params: MachineContext['params'],
27
- expression?: PermissionExpression,
28
29
  user?: MachineContext['user']
29
- ): Promise<boolean> => {
30
- if (!expression || typeof expression === 'boolean') return !!expression
30
+ ) => {
31
31
  const normalizedUser = normalizeUserRole(user)
32
32
 
33
- const value = {
34
- ...params.expansions,
35
- ...params.cursor,
33
+ return {
34
+ ...(params.expansions ?? {}),
35
+ ...(params.cursor ?? {}),
36
36
  '%%root': params.cursor,
37
37
  '%%prevRoot': params.expansions?.['%%prevRoot'],
38
38
  '%%user': normalizedUser,
39
39
  '%%true': true,
40
40
  '%%false': false
41
41
  }
42
+ }
43
+
44
+ const getFunctionCondition = (
45
+ expression: unknown
46
+ ): [string, Record<string, any>] | null => {
47
+ if (!expression || typeof expression !== 'object' || Array.isArray(expression)) {
48
+ return null
49
+ }
50
+
51
+ const entries = Object.entries(expression as Record<string, unknown>)
52
+ if (entries.length !== 1) {
53
+ return null
54
+ }
55
+
56
+ const [key, value] = entries[0]
57
+ if (!functionsConditions.includes(key)) {
58
+ return null
59
+ }
60
+
61
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
62
+ return null
63
+ }
64
+
65
+ return Object.prototype.hasOwnProperty.call(value, '%function')
66
+ ? [key, value as Record<string, any>]
67
+ : null
68
+ }
69
+
70
+ export const evaluateExpandedExpression = async (
71
+ expression: unknown,
72
+ params: MachineContext['params'],
73
+ user?: MachineContext['user']
74
+ ): Promise<boolean> => {
75
+ if (typeof expression === 'boolean') {
76
+ return expression
77
+ }
78
+
79
+ if (!expression || typeof expression !== 'object') {
80
+ return Boolean(expression)
81
+ }
82
+
83
+ const block = expression as Record<string, unknown>
84
+ const functionCondition = getFunctionCondition(block)
85
+
86
+ if (functionCondition) {
87
+ return evaluateComplexExpression(functionCondition, params, user)
88
+ }
89
+
90
+ const andKey = andConditions.find((key) => Object.prototype.hasOwnProperty.call(block, key))
91
+ if (andKey) {
92
+ const conditions = Array.isArray(block[andKey]) ? (block[andKey] as unknown[]) : []
93
+ if (!conditions.length) return true
94
+
95
+ for (const condition of conditions) {
96
+ if (!(await evaluateExpandedExpression(condition, params, user))) {
97
+ return false
98
+ }
99
+ }
100
+
101
+ return true
102
+ }
103
+
104
+ const orKey = orConditions.find((key) => Object.prototype.hasOwnProperty.call(block, key))
105
+ if (orKey) {
106
+ const conditions = Array.isArray(block[orKey]) ? (block[orKey] as unknown[]) : []
107
+ if (!conditions.length) return true
108
+
109
+ for (const condition of conditions) {
110
+ if (await evaluateExpandedExpression(condition, params, user)) {
111
+ return true
112
+ }
113
+ }
114
+
115
+ return false
116
+ }
117
+
118
+ const keys = Object.keys(block)
119
+ if (keys.length > 1) {
120
+ for (const key of keys) {
121
+ if (!(await evaluateExpandedExpression({ [key]: block[key] }, params, user))) {
122
+ return false
123
+ }
124
+ }
125
+
126
+ return true
127
+ }
128
+
129
+ return rulesMatcherUtils.checkRule(block as never, buildEvaluationContext(params, user), {})
130
+ }
131
+
132
+ export const evaluateExpression = async (
133
+ params: MachineContext['params'],
134
+ expression?: PermissionExpression,
135
+ user?: MachineContext['user']
136
+ ): Promise<boolean> => {
137
+ if (!expression || typeof expression === 'boolean') return !!expression
138
+
139
+ const value = buildEvaluationContext(params, user)
42
140
  const conditions = expandQuery(expression, value)
43
- const complexCondition = Object.entries(conditions as Record<string, any>).find(
44
- ([key]) => functionsConditions.includes(key)
45
- )
46
- return complexCondition
47
- ? await evaluateComplexExpression(complexCondition, params, normalizedUser)
48
- : rulesMatcherUtils.checkRule(conditions, value, {})
141
+ return evaluateExpandedExpression(conditions, params, user)
49
142
  }
50
143
 
51
144
  const evaluateComplexExpression = async (
@@ -74,7 +167,7 @@ const evaluateComplexExpression = async (
74
167
  const expandedArguments =
75
168
  fnArguments && fnArguments.length
76
169
  ? ((expandQuery({ args: fnArguments }, expansionContext) as { args: unknown[] })
77
- .args ?? [])
170
+ .args ?? [])
78
171
  : [params.cursor]
79
172
 
80
173
  const response = await GenerateContext({
@@ -2,6 +2,7 @@ import { Document, OptionalId } from 'mongodb'
2
2
  import { User } from '../../../auth/dtos'
3
3
  import { Filter } from '../../../features/rules/interface'
4
4
  import { getValidRule } from '../../../services/mongodb-atlas/utils'
5
+ import { evaluateExpression } from '../helpers'
5
6
  import { Role } from '../interface'
6
7
  import { LogMachineInfoParams } from './interface'
7
8
 
@@ -28,6 +29,20 @@ export const getWinningRole = (
28
29
  return null
29
30
  }
30
31
 
32
+ export const getWinningRoleAsync = async (
33
+ document: OptionalId<Document> | null,
34
+ user: User,
35
+ roles: Role[] = []
36
+ ): Promise<Role | null> => {
37
+ if (!roles.length) return null
38
+ for (const role of roles) {
39
+ if (await checkApplyWhenAsync(role.apply_when, user, document)) {
40
+ return role
41
+ }
42
+ }
43
+ return null
44
+ }
45
+
31
46
  /**
32
47
  * Checks if the `apply_when` condition is valid for the given user and document.
33
48
  *
@@ -50,6 +65,25 @@ export const checkApplyWhen = (
50
65
  return !!validRule.length
51
66
  }
52
67
 
68
+ export const checkApplyWhenAsync = async (
69
+ apply_when: Role['apply_when'],
70
+ user: User,
71
+ document: OptionalId<Document> | null
72
+ ) => {
73
+ return evaluateExpression(
74
+ {
75
+ type: 'read',
76
+ roles: [],
77
+ cursor: document,
78
+ expansions: {
79
+ '%%prevRoot': undefined
80
+ }
81
+ },
82
+ apply_when,
83
+ user
84
+ )
85
+ }
86
+
53
87
  /**
54
88
  * Logs machine step information if logging is enabled.
55
89
  *