@flowerforce/flowerbase 1.8.4-beta.1 → 1.8.4-beta.3
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/functions/interface.d.ts +1 -0
- package/dist/features/functions/interface.d.ts.map +1 -1
- package/dist/features/functions/utils.js +1 -1
- 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/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +91 -14
- 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/functions/interface.ts +4 -1
- package/src/features/functions/utils.ts +3 -3
- package/src/services/mongodb-atlas/index.ts +150 -150
- package/src/utils/__tests__/contextExecuteCompatibility.test.ts +40 -0
- package/src/utils/__tests__/evaluateExpression.test.ts +89 -0
- package/src/utils/__tests__/getWinningRole.test.ts +17 -0
- package/src/utils/context/index.ts +142 -13
- package/src/utils/roles/helpers.ts +107 -14
- package/src/utils/roles/machines/utils.ts +34 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
1
2
|
import { createRequire } from 'node:module'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { pathToFileURL } from 'node:url'
|
|
4
5
|
import vm from 'vm'
|
|
5
6
|
import { EJSON } from 'bson'
|
|
6
7
|
import { StateManager } from '../../state'
|
|
8
|
+
import { Function as AppFunction } from '../../features/functions/interface'
|
|
7
9
|
import { generateContextData } from './helpers'
|
|
8
10
|
import { GenerateContextParams } from './interface'
|
|
9
11
|
|
|
@@ -92,6 +94,42 @@ const wrapEsmModule = (code: string): string => {
|
|
|
92
94
|
return `${prelude}\n${code}\n${trailer}`
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
const transpileSandboxModule = (code: string): string => {
|
|
98
|
+
const exportedNames: string[] = []
|
|
99
|
+
let transformed = code.includes('import ')
|
|
100
|
+
? transformImportsToRequire(code)
|
|
101
|
+
: code
|
|
102
|
+
|
|
103
|
+
transformed = transformed.replace(
|
|
104
|
+
/^\s*export\s+function\s+([A-Za-z_$][\w$]*)\s*\(/gm,
|
|
105
|
+
(_match, name: string) => {
|
|
106
|
+
exportedNames.push(name)
|
|
107
|
+
return `function ${name}(`
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
transformed = transformed.replace(
|
|
112
|
+
/^\s*export\s+(const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm,
|
|
113
|
+
(_match, kind: string, name: string) => {
|
|
114
|
+
exportedNames.push(name)
|
|
115
|
+
return `${kind} ${name}`
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
transformed = transformed.replace(
|
|
120
|
+
/^\s*export\s+default\s+/gm,
|
|
121
|
+
'module.exports = '
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (exportedNames.length === 0) {
|
|
125
|
+
return transformed
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `${transformed}\n${[...new Set(exportedNames)]
|
|
129
|
+
.map((name) => `exports.${name} = ${name}`)
|
|
130
|
+
.join('\n')}`
|
|
131
|
+
}
|
|
132
|
+
|
|
95
133
|
const resolveImportTarget = (specifier: string, customRequire: NodeRequire): string => {
|
|
96
134
|
try {
|
|
97
135
|
const resolved = customRequire.resolve(specifier)
|
|
@@ -123,6 +161,82 @@ type SandboxContext = vm.Context & {
|
|
|
123
161
|
__fb_dirname?: string
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
type SandboxExecutionContext = ReturnType<typeof generateContextData>
|
|
165
|
+
|
|
166
|
+
const resolveModulePath = (specifier: string, parentFile: string): string | undefined => {
|
|
167
|
+
const parentDir = path.dirname(parentFile)
|
|
168
|
+
const basePath = path.resolve(parentDir, specifier)
|
|
169
|
+
const candidates = [
|
|
170
|
+
basePath,
|
|
171
|
+
`${basePath}.js`,
|
|
172
|
+
`${basePath}.ts`,
|
|
173
|
+
path.join(basePath, 'index.js'),
|
|
174
|
+
path.join(basePath, 'index.ts')
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
return candidates.find((candidate) => {
|
|
178
|
+
try {
|
|
179
|
+
return fs.statSync(candidate).isFile()
|
|
180
|
+
} catch {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const executeSandboxModule = ({
|
|
187
|
+
code,
|
|
188
|
+
contextData,
|
|
189
|
+
filePath,
|
|
190
|
+
moduleCache
|
|
191
|
+
}: {
|
|
192
|
+
code: string
|
|
193
|
+
contextData: SandboxExecutionContext
|
|
194
|
+
filePath: string
|
|
195
|
+
moduleCache: Map<string, unknown>
|
|
196
|
+
}): unknown => {
|
|
197
|
+
if (moduleCache.has(filePath)) {
|
|
198
|
+
return moduleCache.get(filePath)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sandboxModule: SandboxModule = { exports: {} }
|
|
202
|
+
moduleCache.set(filePath, sandboxModule.exports)
|
|
203
|
+
const baseRequire = createRequire(filePath)
|
|
204
|
+
|
|
205
|
+
const localRequire = ((specifier: string) => {
|
|
206
|
+
if (specifier.startsWith('.') || specifier.startsWith('/')) {
|
|
207
|
+
const resolvedPath = resolveModulePath(specifier, filePath)
|
|
208
|
+
if (resolvedPath) {
|
|
209
|
+
return executeSandboxModule({
|
|
210
|
+
code: fs.readFileSync(resolvedPath, 'utf-8'),
|
|
211
|
+
contextData,
|
|
212
|
+
filePath: resolvedPath,
|
|
213
|
+
moduleCache
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return baseRequire(specifier)
|
|
219
|
+
}) as NodeRequire
|
|
220
|
+
|
|
221
|
+
const vmContext = vm.createContext({
|
|
222
|
+
...contextData,
|
|
223
|
+
require: localRequire,
|
|
224
|
+
exports: sandboxModule.exports,
|
|
225
|
+
module: sandboxModule,
|
|
226
|
+
__filename: filePath,
|
|
227
|
+
__dirname: path.dirname(filePath),
|
|
228
|
+
__fb_require: localRequire,
|
|
229
|
+
__fb_filename: filePath,
|
|
230
|
+
__fb_dirname: path.dirname(filePath)
|
|
231
|
+
}) as SandboxContext
|
|
232
|
+
|
|
233
|
+
vm.runInContext(transpileSandboxModule(code), vmContext, { filename: filePath })
|
|
234
|
+
sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
|
|
235
|
+
moduleCache.set(filePath, sandboxModule.exports)
|
|
236
|
+
|
|
237
|
+
return sandboxModule.exports
|
|
238
|
+
}
|
|
239
|
+
|
|
126
240
|
const isExportedFunction = (value: unknown): value is ExportedFunction =>
|
|
127
241
|
typeof value === 'function'
|
|
128
242
|
|
|
@@ -141,10 +255,28 @@ const resolveExport = (ctx: SandboxContext): ExportedFunction | undefined => {
|
|
|
141
255
|
return getDefaultExport(moduleExports) ?? getDefaultExport(contextExports)
|
|
142
256
|
}
|
|
143
257
|
|
|
144
|
-
const buildVmContext = (
|
|
258
|
+
const buildVmContext = (
|
|
259
|
+
contextData: ReturnType<typeof generateContextData>,
|
|
260
|
+
currentFunction?: AppFunction
|
|
261
|
+
) => {
|
|
145
262
|
const sandboxModule: SandboxModule = { exports: {} }
|
|
146
|
-
const entryFile = require.main?.filename ?? process.cwd()
|
|
147
|
-
const
|
|
263
|
+
const entryFile = currentFunction?.sourcePath ?? require.main?.filename ?? process.cwd()
|
|
264
|
+
const moduleCache = new Map<string, unknown>()
|
|
265
|
+
const customRequire = ((specifier: string) => {
|
|
266
|
+
if ((specifier.startsWith('.') || specifier.startsWith('/')) && currentFunction?.sourcePath) {
|
|
267
|
+
const resolvedPath = resolveModulePath(specifier, currentFunction.sourcePath)
|
|
268
|
+
if (resolvedPath) {
|
|
269
|
+
return executeSandboxModule({
|
|
270
|
+
code: fs.readFileSync(resolvedPath, 'utf-8'),
|
|
271
|
+
contextData,
|
|
272
|
+
filePath: resolvedPath,
|
|
273
|
+
moduleCache
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return createRequire(entryFile)(specifier)
|
|
279
|
+
}) as NodeRequire
|
|
148
280
|
|
|
149
281
|
const vmContext: SandboxContext = vm.createContext({
|
|
150
282
|
...contextData,
|
|
@@ -206,7 +338,10 @@ export async function GenerateContext({
|
|
|
206
338
|
GenerateContextSync,
|
|
207
339
|
request
|
|
208
340
|
})
|
|
209
|
-
const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(
|
|
341
|
+
const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(
|
|
342
|
+
contextData,
|
|
343
|
+
functionToRun
|
|
344
|
+
)
|
|
210
345
|
|
|
211
346
|
const vmModules = vm as typeof vm & {
|
|
212
347
|
SourceTextModule?: typeof vm.SourceTextModule
|
|
@@ -271,10 +406,7 @@ export async function GenerateContext({
|
|
|
271
406
|
}
|
|
272
407
|
|
|
273
408
|
if (!usedVmModules) {
|
|
274
|
-
|
|
275
|
-
? transformImportsToRequire(functionToRun.code)
|
|
276
|
-
: functionToRun.code
|
|
277
|
-
vm.runInContext(codeToRun, vmContext)
|
|
409
|
+
vm.runInContext(transpileSandboxModule(functionToRun.code), vmContext, { filename: entryFile })
|
|
278
410
|
}
|
|
279
411
|
|
|
280
412
|
sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
|
|
@@ -323,12 +455,9 @@ export function GenerateContextSync({
|
|
|
323
455
|
GenerateContextSync,
|
|
324
456
|
request
|
|
325
457
|
})
|
|
326
|
-
const { sandboxModule, vmContext } = buildVmContext(contextData)
|
|
327
|
-
const codeToRun = functionToRun.code.includes('import ')
|
|
328
|
-
? transformImportsToRequire(functionToRun.code)
|
|
329
|
-
: functionToRun.code
|
|
458
|
+
const { sandboxModule, entryFile, vmContext } = buildVmContext(contextData, functionToRun)
|
|
330
459
|
|
|
331
|
-
vm.runInContext(
|
|
460
|
+
vm.runInContext(transpileSandboxModule(functionToRun.code), vmContext, { filename: entryFile })
|
|
332
461
|
sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
|
|
333
462
|
const fn = sandboxModule.exports as ExportedFunction
|
|
334
463
|
if (deserializeArgs) {
|
|
@@ -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
|
*
|