@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.
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +28 -18
- package/dist/monitoring/utils.d.ts +1 -1
- package/dist/services/mongodb-atlas/index.js +11 -11
- package/dist/utils/context/helpers.d.ts +3 -3
- package/dist/utils/roles/helpers.d.ts +1 -0
- package/dist/utils/roles/helpers.d.ts.map +1 -1
- package/dist/utils/roles/helpers.js +77 -8
- package/dist/utils/roles/machines/utils.d.ts +2 -0
- package/dist/utils/roles/machines/utils.d.ts.map +1 -1
- package/dist/utils/roles/machines/utils.js +33 -1
- package/package.json +1 -1
- package/src/features/triggers/__tests__/utils.test.ts +112 -0
- package/src/features/triggers/utils.ts +30 -18
- package/src/services/mongodb-atlas/index.ts +150 -150
- package/src/utils/__tests__/evaluateExpression.test.ts +89 -0
- package/src/utils/__tests__/getWinningRole.test.ts +17 -0
- package/src/utils/roles/helpers.ts +107 -14
- package/src/utils/roles/machines/utils.ts +34 -0
|
@@ -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
|
-
|
|
27
|
+
const buildEvaluationContext = (
|
|
26
28
|
params: MachineContext['params'],
|
|
27
|
-
expression?: PermissionExpression,
|
|
28
29
|
user?: MachineContext['user']
|
|
29
|
-
)
|
|
30
|
-
if (!expression || typeof expression === 'boolean') return !!expression
|
|
30
|
+
) => {
|
|
31
31
|
const normalizedUser = normalizeUserRole(user)
|
|
32
32
|
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*
|