@flowerforce/flowerbase 1.7.5-beta.2 → 1.7.5-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/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +14 -3
- package/dist/services/api/index.d.ts +4 -0
- package/dist/services/api/index.d.ts.map +1 -1
- package/dist/services/api/utils.d.ts +1 -0
- package/dist/services/api/utils.d.ts.map +1 -1
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +17 -1
- package/dist/utils/context/helpers.d.ts +12 -0
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/roles/helpers.d.ts.map +1 -1
- package/dist/utils/roles/helpers.js +19 -4
- package/dist/utils/roles/interface.d.ts +10 -6
- package/dist/utils/roles/interface.d.ts.map +1 -1
- package/dist/utils/roles/machines/commonValidators.js +2 -2
- package/dist/utils/roles/machines/fieldPermissions.d.ts +8 -0
- package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -0
- package/dist/utils/roles/machines/fieldPermissions.js +67 -0
- package/dist/utils/roles/machines/read/A/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/A/index.js +4 -3
- package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/C/index.js +16 -16
- package/dist/utils/roles/machines/read/C/validators.js +2 -2
- package/dist/utils/roles/machines/read/D/index.js +1 -1
- package/dist/utils/roles/machines/read/D/validators.d.ts +1 -1
- package/dist/utils/roles/machines/read/D/validators.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/D/validators.js +19 -21
- package/dist/utils/roles/machines/write/B/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/write/B/index.js +12 -9
- package/dist/utils/roles/machines/write/C/index.js +1 -1
- package/dist/utils/roles/machines/write/C/validators.d.ts +1 -1
- package/dist/utils/roles/machines/write/C/validators.d.ts.map +1 -1
- package/dist/utils/roles/machines/write/C/validators.js +16 -21
- package/package.json +1 -1
- package/src/features/functions/controller.ts +16 -3
- package/src/features/triggers/__tests__/index.test.ts +2 -2
- package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +1 -1
- package/src/services/mongodb-atlas/utils.ts +19 -4
- package/src/utils/__tests__/STEP_A_STATES.test.ts +24 -2
- package/src/utils/__tests__/STEP_C_STATES.test.ts +61 -27
- package/src/utils/__tests__/STEP_D_STATES.test.ts +9 -9
- package/src/utils/__tests__/WRITE_STEP_B_STATES.test.ts +184 -0
- package/src/utils/__tests__/checkAdditionalFieldsFn.test.ts +2 -2
- package/src/utils/__tests__/checkFieldsPropertyExists.test.ts +13 -0
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +52 -121
- package/src/utils/__tests__/evaluateTopLevelReadFn.test.ts +10 -1
- package/src/utils/__tests__/evaluateTopLevelWriteFn.test.ts +21 -5
- package/src/utils/roles/helpers.ts +18 -4
- package/src/utils/roles/interface.ts +13 -6
- package/src/utils/roles/machines/commonValidators.ts +1 -1
- package/src/utils/roles/machines/fieldPermissions.ts +86 -0
- package/src/utils/roles/machines/read/A/index.ts +4 -3
- package/src/utils/roles/machines/read/C/index.ts +18 -18
- package/src/utils/roles/machines/read/C/validators.ts +2 -2
- package/src/utils/roles/machines/read/D/index.ts +1 -1
- package/src/utils/roles/machines/read/D/validators.ts +12 -25
- package/src/utils/roles/machines/write/B/index.ts +12 -9
- package/src/utils/roles/machines/write/C/index.ts +1 -1
- package/src/utils/roles/machines/write/C/validators.ts +9 -26
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { MachineContext } from '../roles/machines/interface'
|
|
2
|
+
import { STEP_B_STATES } from '../roles/machines/write/B'
|
|
3
|
+
import {
|
|
4
|
+
checkFieldsPropertyExists,
|
|
5
|
+
evaluateTopLevelPermissionsFn
|
|
6
|
+
} from '../roles/machines/commonValidators'
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
checkDeleteRequest,
|
|
10
|
+
evaluateTopLevelDelete,
|
|
11
|
+
evaluateTopLevelWrite,
|
|
12
|
+
evaluateTopLevelInsert,
|
|
13
|
+
checkFieldsProperty
|
|
14
|
+
} = STEP_B_STATES
|
|
15
|
+
|
|
16
|
+
jest.mock('../roles/machines/commonValidators', () => ({
|
|
17
|
+
checkFieldsPropertyExists: jest.fn(),
|
|
18
|
+
evaluateTopLevelPermissionsFn: jest.fn()
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
const endValidation = jest.fn()
|
|
22
|
+
const goToNextValidationStage = jest.fn()
|
|
23
|
+
const next = jest.fn()
|
|
24
|
+
|
|
25
|
+
describe('WRITE STEP_B_STATES', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('routes delete requests to evaluateTopLevelDelete', async () => {
|
|
31
|
+
const context = { params: { type: 'delete' } } as MachineContext
|
|
32
|
+
await checkDeleteRequest({
|
|
33
|
+
context,
|
|
34
|
+
endValidation,
|
|
35
|
+
goToNextValidationStage,
|
|
36
|
+
next,
|
|
37
|
+
initialStep: null
|
|
38
|
+
})
|
|
39
|
+
expect(next).toHaveBeenCalledWith('evaluateTopLevelDelete')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('routes non-delete requests to evaluateTopLevelWrite', async () => {
|
|
43
|
+
const context = { params: { type: 'write' } } as MachineContext
|
|
44
|
+
await checkDeleteRequest({
|
|
45
|
+
context,
|
|
46
|
+
endValidation,
|
|
47
|
+
goToNextValidationStage,
|
|
48
|
+
next,
|
|
49
|
+
initialStep: null
|
|
50
|
+
})
|
|
51
|
+
expect(next).toHaveBeenCalledWith('evaluateTopLevelWrite')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('allows delete only when top-level delete is true', async () => {
|
|
55
|
+
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
|
|
56
|
+
const context = {} as MachineContext
|
|
57
|
+
await evaluateTopLevelDelete({
|
|
58
|
+
context,
|
|
59
|
+
endValidation,
|
|
60
|
+
goToNextValidationStage,
|
|
61
|
+
next,
|
|
62
|
+
initialStep: null
|
|
63
|
+
})
|
|
64
|
+
expect(endValidation).toHaveBeenCalledWith({ success: true })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('denies delete when top-level delete is false/undefined', async () => {
|
|
68
|
+
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(undefined)
|
|
69
|
+
const context = {} as MachineContext
|
|
70
|
+
await evaluateTopLevelDelete({
|
|
71
|
+
context,
|
|
72
|
+
endValidation,
|
|
73
|
+
goToNextValidationStage,
|
|
74
|
+
next,
|
|
75
|
+
initialStep: null
|
|
76
|
+
})
|
|
77
|
+
expect(endValidation).toHaveBeenCalledWith({ success: false })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('routes to insert check when write=true', async () => {
|
|
81
|
+
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
|
|
82
|
+
const context = {} as MachineContext
|
|
83
|
+
await evaluateTopLevelWrite({
|
|
84
|
+
context,
|
|
85
|
+
endValidation,
|
|
86
|
+
goToNextValidationStage,
|
|
87
|
+
next,
|
|
88
|
+
initialStep: null
|
|
89
|
+
})
|
|
90
|
+
expect(next).toHaveBeenCalledWith('evaluateTopLevelInsert')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('routes to insert check when write=true and field-level rules exist', async () => {
|
|
94
|
+
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
|
|
95
|
+
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
|
|
96
|
+
const context = {} as MachineContext
|
|
97
|
+
await evaluateTopLevelWrite({
|
|
98
|
+
context,
|
|
99
|
+
endValidation,
|
|
100
|
+
goToNextValidationStage,
|
|
101
|
+
next,
|
|
102
|
+
initialStep: null
|
|
103
|
+
})
|
|
104
|
+
expect(next).toHaveBeenCalledWith('evaluateTopLevelInsert')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('denies when write=false', async () => {
|
|
108
|
+
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(false)
|
|
109
|
+
const context = {} as MachineContext
|
|
110
|
+
await evaluateTopLevelWrite({
|
|
111
|
+
context,
|
|
112
|
+
endValidation,
|
|
113
|
+
goToNextValidationStage,
|
|
114
|
+
next,
|
|
115
|
+
initialStep: null
|
|
116
|
+
})
|
|
117
|
+
expect(endValidation).toHaveBeenCalledWith({ success: false })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('routes to field-level checks when write is undefined', async () => {
|
|
121
|
+
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(undefined)
|
|
122
|
+
const context = {} as MachineContext
|
|
123
|
+
await evaluateTopLevelWrite({
|
|
124
|
+
context,
|
|
125
|
+
endValidation,
|
|
126
|
+
goToNextValidationStage,
|
|
127
|
+
next,
|
|
128
|
+
initialStep: null
|
|
129
|
+
})
|
|
130
|
+
expect(next).toHaveBeenCalledWith('checkFieldsProperty')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('denies insert when insert is false/undefined', async () => {
|
|
134
|
+
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(false)
|
|
135
|
+
const context = {} as MachineContext
|
|
136
|
+
await evaluateTopLevelInsert({
|
|
137
|
+
context,
|
|
138
|
+
endValidation,
|
|
139
|
+
goToNextValidationStage,
|
|
140
|
+
next,
|
|
141
|
+
initialStep: null
|
|
142
|
+
})
|
|
143
|
+
expect(endValidation).toHaveBeenCalledWith({ success: false })
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('allows when insert=true', async () => {
|
|
147
|
+
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
|
|
148
|
+
const context = {} as MachineContext
|
|
149
|
+
await evaluateTopLevelInsert({
|
|
150
|
+
context,
|
|
151
|
+
endValidation,
|
|
152
|
+
goToNextValidationStage,
|
|
153
|
+
next,
|
|
154
|
+
initialStep: null
|
|
155
|
+
})
|
|
156
|
+
expect(endValidation).toHaveBeenCalledWith({ success: true })
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('routes checkFieldsProperty to checkIsValidFieldName when field rules exist', async () => {
|
|
160
|
+
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
|
|
161
|
+
const context = {} as MachineContext
|
|
162
|
+
await checkFieldsProperty({
|
|
163
|
+
context,
|
|
164
|
+
endValidation,
|
|
165
|
+
goToNextValidationStage,
|
|
166
|
+
next,
|
|
167
|
+
initialStep: null
|
|
168
|
+
})
|
|
169
|
+
expect(goToNextValidationStage).toHaveBeenCalledWith('checkIsValidFieldName')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('routes checkFieldsProperty to checkAdditionalFields when no field rules exist', async () => {
|
|
173
|
+
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(false)
|
|
174
|
+
const context = {} as MachineContext
|
|
175
|
+
await checkFieldsProperty({
|
|
176
|
+
context,
|
|
177
|
+
endValidation,
|
|
178
|
+
goToNextValidationStage,
|
|
179
|
+
next,
|
|
180
|
+
initialStep: null
|
|
181
|
+
})
|
|
182
|
+
expect(goToNextValidationStage).toHaveBeenCalledWith('checkAdditionalFields')
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -34,12 +34,12 @@ describe('comparePassword', () => {
|
|
|
34
34
|
})
|
|
35
35
|
expect(isDefined).toBe(false)
|
|
36
36
|
})
|
|
37
|
-
it('should return
|
|
37
|
+
it('should return true for empty additional fields (defined)', () => {
|
|
38
38
|
const isDefined = checkAdditionalFieldsFn({
|
|
39
39
|
role: { ...mockedRole, additional_fields: {} },
|
|
40
40
|
user: mockUser,
|
|
41
41
|
params: mockParams
|
|
42
42
|
})
|
|
43
|
-
expect(isDefined).toBe(
|
|
43
|
+
expect(isDefined).toBe(true)
|
|
44
44
|
})
|
|
45
45
|
})
|
|
@@ -44,4 +44,17 @@ describe('checkFieldsPropertyExists', () => {
|
|
|
44
44
|
})
|
|
45
45
|
expect(isValid).toBe(true)
|
|
46
46
|
})
|
|
47
|
+
it('should return true if additional_fields is defined (even empty)', () => {
|
|
48
|
+
const isValid = checkFieldsPropertyExists({
|
|
49
|
+
role: {
|
|
50
|
+
fields: {},
|
|
51
|
+
additional_fields: {},
|
|
52
|
+
apply_when: {},
|
|
53
|
+
name: 'test'
|
|
54
|
+
} as Role,
|
|
55
|
+
user: mockUser,
|
|
56
|
+
params: mockParams
|
|
57
|
+
})
|
|
58
|
+
expect(isValid).toBe(true)
|
|
59
|
+
})
|
|
47
60
|
})
|
|
@@ -11,17 +11,16 @@ describe('checkIsValidFieldNameFn', () => {
|
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
jest.clearAllMocks()
|
|
13
13
|
})
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
it('returns only explicitly allowed fields when no fallback is configured', async () => {
|
|
15
16
|
const mockedRole = {
|
|
16
17
|
name: 'test',
|
|
17
|
-
apply_when: {
|
|
18
|
-
'%%true': true
|
|
19
|
-
},
|
|
18
|
+
apply_when: { '%%true': true },
|
|
20
19
|
fields: {
|
|
21
20
|
name: { read: true, write: false },
|
|
22
|
-
email: { read: false, write: true }
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
email: { read: false, write: true },
|
|
22
|
+
age: { read: false, write: false }
|
|
23
|
+
}
|
|
25
24
|
} as Role
|
|
26
25
|
const context = {
|
|
27
26
|
user: mockUser,
|
|
@@ -29,168 +28,100 @@ describe('checkIsValidFieldNameFn', () => {
|
|
|
29
28
|
params: {
|
|
30
29
|
cursor: { _id: mockId, name: 'Alice', email: 'alice@example.com', age: 25 }
|
|
31
30
|
}
|
|
32
|
-
}
|
|
31
|
+
} as MachineContext
|
|
33
32
|
|
|
34
|
-
const result = checkIsValidFieldNameFn(context
|
|
33
|
+
const result = await checkIsValidFieldNameFn(context)
|
|
35
34
|
expect(result).toEqual({
|
|
36
|
-
_id: mockId,
|
|
37
35
|
name: 'Alice',
|
|
38
|
-
email: 'alice@example.com'
|
|
39
|
-
age: 25
|
|
36
|
+
email: 'alice@example.com'
|
|
40
37
|
})
|
|
41
38
|
})
|
|
42
|
-
it("should exclude _id if role doesn't allows it", () => {
|
|
43
|
-
const mockedRole = {
|
|
44
|
-
name: 'test',
|
|
45
|
-
apply_when: {
|
|
46
|
-
'%%true': true
|
|
47
|
-
},
|
|
48
|
-
fields: {
|
|
49
|
-
_id: { read: false, write: false },
|
|
50
|
-
name: { read: true, write: false }
|
|
51
|
-
},
|
|
52
|
-
additional_fields: {}
|
|
53
|
-
} as Role
|
|
54
|
-
const context = {
|
|
55
|
-
user: mockUser,
|
|
56
|
-
role: mockedRole,
|
|
57
|
-
params: {
|
|
58
|
-
cursor: { _id: mockId, name: 'Alice' }
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
expect(result).toEqual({ name: 'Alice' })
|
|
65
|
-
})
|
|
66
|
-
it('should include _id if write role allows it', () => {
|
|
40
|
+
it('uses per-field additional_fields as fallback for unknown fields', async () => {
|
|
67
41
|
const mockedRole = {
|
|
68
42
|
name: 'test',
|
|
69
|
-
apply_when: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
name: { read: true, write: false }
|
|
75
|
-
},
|
|
76
|
-
additional_fields: {}
|
|
77
|
-
} as Role
|
|
78
|
-
const context = {
|
|
79
|
-
user: mockUser,
|
|
80
|
-
role: mockedRole,
|
|
81
|
-
params: {
|
|
82
|
-
cursor: { _id: mockId, name: 'Alice' }
|
|
43
|
+
apply_when: { '%%true': true },
|
|
44
|
+
fields: {},
|
|
45
|
+
additional_fields: {
|
|
46
|
+
phone: { read: true, write: false },
|
|
47
|
+
address: { read: false, write: true }
|
|
83
48
|
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const result = checkIsValidFieldNameFn(context as MachineContext)
|
|
87
|
-
|
|
88
|
-
expect(result).toEqual({ _id: mockId, name: 'Alice' })
|
|
89
|
-
})
|
|
90
|
-
it('should include _id if read role allows it', () => {
|
|
91
|
-
const mockedRole = {
|
|
92
|
-
name: 'test',
|
|
93
|
-
apply_when: {
|
|
94
|
-
'%%true': true
|
|
95
|
-
},
|
|
96
|
-
fields: {
|
|
97
|
-
_id: { read: true, write: false },
|
|
98
|
-
name: { read: true, write: false }
|
|
99
|
-
},
|
|
100
|
-
additional_fields: {}
|
|
101
49
|
} as Role
|
|
102
50
|
const context = {
|
|
103
|
-
user: mockUser,
|
|
104
51
|
role: mockedRole,
|
|
105
52
|
params: {
|
|
106
|
-
cursor: { _id: mockId,
|
|
53
|
+
cursor: { _id: mockId, phone: '123456789', address: 'Unknown', city: 'Rome' }
|
|
107
54
|
}
|
|
108
|
-
}
|
|
55
|
+
} as MachineContext
|
|
109
56
|
|
|
110
|
-
const result = checkIsValidFieldNameFn(context
|
|
111
|
-
|
|
112
|
-
|
|
57
|
+
const result = await checkIsValidFieldNameFn(context)
|
|
58
|
+
expect(result).toEqual({
|
|
59
|
+
phone: '123456789',
|
|
60
|
+
address: 'Unknown'
|
|
61
|
+
})
|
|
113
62
|
})
|
|
114
63
|
|
|
115
|
-
it('
|
|
64
|
+
it('supports realm-style global additional_fields fallback', async () => {
|
|
116
65
|
const mockedRole = {
|
|
117
|
-
name: '
|
|
118
|
-
apply_when: {
|
|
119
|
-
'%%true': true
|
|
120
|
-
},
|
|
66
|
+
name: 'collaborator',
|
|
67
|
+
apply_when: { '%%true': true },
|
|
121
68
|
fields: {
|
|
122
|
-
|
|
69
|
+
roles: { read: true, write: false }
|
|
123
70
|
},
|
|
124
|
-
additional_fields: {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
role: mockedRole,
|
|
128
|
-
params: {
|
|
129
|
-
cursor: { name: 'Charlie', email: 'charlie@example.com' }
|
|
71
|
+
additional_fields: {
|
|
72
|
+
read: false,
|
|
73
|
+
write: false
|
|
130
74
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const result = checkIsValidFieldNameFn(context as MachineContext)
|
|
134
|
-
|
|
135
|
-
expect(result).toEqual({ email: 'charlie@example.com' })
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('should handle additional_fields correctly for read permission', () => {
|
|
139
|
-
const mockedRole = {
|
|
140
|
-
name: 'test',
|
|
141
|
-
apply_when: {
|
|
142
|
-
'%%true': true
|
|
143
|
-
},
|
|
144
|
-
fields: {},
|
|
145
|
-
additional_fields: { phone: { read: true, write: false } }
|
|
146
75
|
} as Role
|
|
147
76
|
const context = {
|
|
148
77
|
role: mockedRole,
|
|
149
78
|
params: {
|
|
150
|
-
cursor: {
|
|
79
|
+
cursor: { roles: ['editor'], email: 'user@example.com' }
|
|
151
80
|
}
|
|
152
|
-
}
|
|
81
|
+
} as MachineContext
|
|
153
82
|
|
|
154
|
-
const result = checkIsValidFieldNameFn(context
|
|
155
|
-
expect(result).toEqual({
|
|
83
|
+
const result = await checkIsValidFieldNameFn(context)
|
|
84
|
+
expect(result).toEqual({
|
|
85
|
+
roles: ['editor']
|
|
86
|
+
})
|
|
156
87
|
})
|
|
157
|
-
|
|
88
|
+
|
|
89
|
+
it('denies unknown fields when additional_fields global fallback is false', async () => {
|
|
158
90
|
const mockedRole = {
|
|
159
91
|
name: 'test',
|
|
160
|
-
apply_when: {
|
|
161
|
-
|
|
92
|
+
apply_when: { '%%true': true },
|
|
93
|
+
fields: {
|
|
94
|
+
roles: { read: true }
|
|
162
95
|
},
|
|
163
|
-
fields: {},
|
|
164
96
|
additional_fields: {
|
|
165
|
-
|
|
166
|
-
|
|
97
|
+
read: false,
|
|
98
|
+
write: false
|
|
167
99
|
}
|
|
168
100
|
} as Role
|
|
169
101
|
const context = {
|
|
170
102
|
role: mockedRole,
|
|
171
103
|
params: {
|
|
172
|
-
cursor: {
|
|
104
|
+
cursor: { email: 'user@example.com' }
|
|
173
105
|
}
|
|
174
|
-
}
|
|
106
|
+
} as MachineContext
|
|
175
107
|
|
|
176
|
-
const result = checkIsValidFieldNameFn(context
|
|
177
|
-
expect(result).toEqual({
|
|
108
|
+
const result = await checkIsValidFieldNameFn(context)
|
|
109
|
+
expect(result).toEqual({})
|
|
178
110
|
})
|
|
179
|
-
|
|
111
|
+
|
|
112
|
+
it('returns empty object when no field permissions are available', async () => {
|
|
180
113
|
const mockedRole = {
|
|
181
114
|
name: 'test',
|
|
182
|
-
apply_when: {
|
|
183
|
-
'%%true': true
|
|
184
|
-
}
|
|
115
|
+
apply_when: { '%%true': true }
|
|
185
116
|
} as Role
|
|
186
117
|
const context = {
|
|
187
118
|
role: mockedRole,
|
|
188
119
|
params: {
|
|
189
120
|
cursor: { _id: mockId, phone: '123456789', address: 'Unknown' }
|
|
190
121
|
}
|
|
191
|
-
}
|
|
122
|
+
} as MachineContext
|
|
192
123
|
|
|
193
|
-
const result = checkIsValidFieldNameFn(context
|
|
194
|
-
expect(result).toEqual({
|
|
124
|
+
const result = await checkIsValidFieldNameFn(context)
|
|
125
|
+
expect(result).toEqual({})
|
|
195
126
|
})
|
|
196
127
|
})
|
|
@@ -17,7 +17,7 @@ describe('evaluateTopLevelReadFn', () => {
|
|
|
17
17
|
beforeEach(() => {
|
|
18
18
|
jest.clearAllMocks()
|
|
19
19
|
})
|
|
20
|
-
it('should return false if type is
|
|
20
|
+
it('should return false if type is neither read nor search', async () => {
|
|
21
21
|
const isValid = await evaluateTopLevelReadFn({
|
|
22
22
|
role: mockedRole,
|
|
23
23
|
user: mockUser,
|
|
@@ -35,6 +35,15 @@ describe('evaluateTopLevelReadFn', () => {
|
|
|
35
35
|
expect(isValid).toBe(undefined)
|
|
36
36
|
expect(evaluateExpression).not.toHaveBeenCalled()
|
|
37
37
|
})
|
|
38
|
+
it('should return undefined if type is search and role read is not defined', async () => {
|
|
39
|
+
const isValid = await evaluateTopLevelReadFn({
|
|
40
|
+
role: mockedRole,
|
|
41
|
+
user: mockUser,
|
|
42
|
+
params: { type: 'search' } as Params
|
|
43
|
+
})
|
|
44
|
+
expect(isValid).toBe(undefined)
|
|
45
|
+
expect(evaluateExpression).not.toHaveBeenCalled()
|
|
46
|
+
})
|
|
38
47
|
it('should return false if type is read and role read defined but evaluate expression returns false', async () => {
|
|
39
48
|
(evaluateExpression as jest.Mock).mockResolvedValueOnce(false)
|
|
40
49
|
const isValid = await evaluateTopLevelReadFn({
|
|
@@ -14,7 +14,10 @@ const mockParams = {
|
|
|
14
14
|
} as Params
|
|
15
15
|
|
|
16
16
|
describe('evaluateTopLevelWriteFn', () => {
|
|
17
|
-
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks()
|
|
19
|
+
})
|
|
20
|
+
it('should return undefined if type is different from read, search and write', async () => {
|
|
18
21
|
const isValid = await evaluateTopLevelWriteFn({
|
|
19
22
|
role: mockedRole,
|
|
20
23
|
user: mockUser,
|
|
@@ -23,6 +26,17 @@ describe('evaluateTopLevelWriteFn', () => {
|
|
|
23
26
|
expect(isValid).toBe(undefined)
|
|
24
27
|
expect(evaluateExpression).not.toHaveBeenCalled()
|
|
25
28
|
})
|
|
29
|
+
it('should evaluate write for search requests', async () => {
|
|
30
|
+
(evaluateExpression as jest.Mock).mockResolvedValueOnce(true)
|
|
31
|
+
const searchParams = { type: 'search' } as Params
|
|
32
|
+
const isValid = await evaluateTopLevelWriteFn({
|
|
33
|
+
role: { ...mockedRole, write: true },
|
|
34
|
+
user: mockUser,
|
|
35
|
+
params: searchParams
|
|
36
|
+
})
|
|
37
|
+
expect(isValid).toBe(true)
|
|
38
|
+
expect(evaluateExpression).toHaveBeenCalledWith(searchParams, true, mockUser)
|
|
39
|
+
})
|
|
26
40
|
it('should return false if type is read but evaluate expression returns false', async () => {
|
|
27
41
|
(evaluateExpression as jest.Mock).mockResolvedValueOnce(false)
|
|
28
42
|
const isValid = await evaluateTopLevelWriteFn({
|
|
@@ -45,22 +59,24 @@ describe('evaluateTopLevelWriteFn', () => {
|
|
|
45
59
|
})
|
|
46
60
|
it('should return false if type is write but evaluate expression returns false', async () => {
|
|
47
61
|
(evaluateExpression as jest.Mock).mockResolvedValueOnce(false)
|
|
62
|
+
const writeParams = { type: 'write' } as Params
|
|
48
63
|
const isValid = await evaluateTopLevelWriteFn({
|
|
49
64
|
role: { ...mockedRole, write: false },
|
|
50
65
|
user: mockUser,
|
|
51
|
-
params:
|
|
66
|
+
params: writeParams
|
|
52
67
|
})
|
|
53
68
|
expect(isValid).toBe(false)
|
|
54
|
-
expect(evaluateExpression).toHaveBeenCalledWith(
|
|
69
|
+
expect(evaluateExpression).toHaveBeenCalledWith(writeParams, false, mockUser)
|
|
55
70
|
})
|
|
56
71
|
it('should return true if type is write and evaluate expression returns true', async () => {
|
|
57
72
|
(evaluateExpression as jest.Mock).mockResolvedValueOnce(true)
|
|
73
|
+
const writeParams = { type: 'write' } as Params
|
|
58
74
|
const isValid = await evaluateTopLevelWriteFn({
|
|
59
75
|
role: { ...mockedRole, write: false },
|
|
60
76
|
user: mockUser,
|
|
61
|
-
params:
|
|
77
|
+
params: writeParams
|
|
62
78
|
})
|
|
63
79
|
expect(isValid).toBe(true)
|
|
64
|
-
expect(evaluateExpression).toHaveBeenCalledWith(
|
|
80
|
+
expect(evaluateExpression).toHaveBeenCalledWith(writeParams, false, mockUser)
|
|
65
81
|
})
|
|
66
82
|
})
|
|
@@ -8,17 +8,30 @@ import { MachineContext } from './machines/interface'
|
|
|
8
8
|
|
|
9
9
|
const functionsConditions = ['%%true', '%%false']
|
|
10
10
|
|
|
11
|
+
const normalizeUserRole = (user?: MachineContext['user']) => {
|
|
12
|
+
if (!user) return user
|
|
13
|
+
if (typeof user !== 'object') return user
|
|
14
|
+
const candidate = user as Record<string, unknown>
|
|
15
|
+
if (typeof candidate.role === 'string') return user
|
|
16
|
+
const customRole =
|
|
17
|
+
typeof candidate.custom_data === 'object' && candidate.custom_data !== null
|
|
18
|
+
? (candidate.custom_data as Record<string, unknown>).role
|
|
19
|
+
: undefined
|
|
20
|
+
return typeof customRole === 'string' ? ({ ...candidate, role: customRole } as MachineContext['user']) : user
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export const evaluateExpression = async (
|
|
12
24
|
params: MachineContext['params'],
|
|
13
25
|
expression?: PermissionExpression,
|
|
14
26
|
user?: MachineContext['user']
|
|
15
27
|
): Promise<boolean> => {
|
|
16
28
|
if (!expression || typeof expression === 'boolean') return !!expression
|
|
29
|
+
const normalizedUser = normalizeUserRole(user)
|
|
17
30
|
|
|
18
31
|
const value = {
|
|
19
32
|
...params.expansions,
|
|
20
33
|
...params.cursor,
|
|
21
|
-
'%%user':
|
|
34
|
+
'%%user': normalizedUser,
|
|
22
35
|
'%%true': true
|
|
23
36
|
}
|
|
24
37
|
const conditions = expandQuery(expression, value)
|
|
@@ -26,7 +39,7 @@ export const evaluateExpression = async (
|
|
|
26
39
|
functionsConditions.includes(key)
|
|
27
40
|
)
|
|
28
41
|
return complexCondition
|
|
29
|
-
? await evaluateComplexExpression(complexCondition, params,
|
|
42
|
+
? await evaluateComplexExpression(complexCondition, params, normalizedUser)
|
|
30
43
|
: rulesMatcherUtils.checkRule(conditions, value, {})
|
|
31
44
|
}
|
|
32
45
|
|
|
@@ -36,6 +49,7 @@ const evaluateComplexExpression = async (
|
|
|
36
49
|
user: MachineContext['user']
|
|
37
50
|
): Promise<boolean> => {
|
|
38
51
|
const [key, config] = condition
|
|
52
|
+
const normalizedUser = normalizeUserRole(user)
|
|
39
53
|
|
|
40
54
|
const functionConfig = config['%function']
|
|
41
55
|
const { name, arguments: fnArguments } = functionConfig
|
|
@@ -47,7 +61,7 @@ const evaluateComplexExpression = async (
|
|
|
47
61
|
...params.expansions,
|
|
48
62
|
...params.cursor,
|
|
49
63
|
'%%root': params.cursor,
|
|
50
|
-
'%%user':
|
|
64
|
+
'%%user': normalizedUser,
|
|
51
65
|
'%%true': true,
|
|
52
66
|
'%%false': false
|
|
53
67
|
}
|
|
@@ -62,7 +76,7 @@ const evaluateComplexExpression = async (
|
|
|
62
76
|
args: expandedArguments,
|
|
63
77
|
app,
|
|
64
78
|
rules: StateManager.select("rules"),
|
|
65
|
-
user,
|
|
79
|
+
user: normalizedUser,
|
|
66
80
|
currentFunction,
|
|
67
81
|
functionName: name,
|
|
68
82
|
functionsList,
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
export type PermissionExpression = boolean
|
|
1
|
+
export type PermissionExpression = boolean | Record<string, unknown>
|
|
2
2
|
|
|
3
3
|
export type FieldPermissionExpression = {
|
|
4
|
-
read?:
|
|
5
|
-
write?:
|
|
4
|
+
read?: PermissionExpression
|
|
5
|
+
write?: PermissionExpression
|
|
6
|
+
fields?: {
|
|
7
|
+
[K: string]: FieldPermissionExpression
|
|
8
|
+
}
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
export type AdditionalFieldsPermissionExpression =
|
|
12
|
+
| FieldPermissionExpression
|
|
13
|
+
| {
|
|
14
|
+
[K: string]: FieldPermissionExpression
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export interface DocumentFiltersPermissions {
|
|
9
18
|
read?: PermissionExpression
|
|
10
19
|
write?: PermissionExpression
|
|
@@ -23,9 +32,7 @@ export interface Role {
|
|
|
23
32
|
fields?: {
|
|
24
33
|
[K: string]: FieldPermissionExpression
|
|
25
34
|
}
|
|
26
|
-
additional_fields?:
|
|
27
|
-
[K: string]: FieldPermissionExpression
|
|
28
|
-
}
|
|
35
|
+
additional_fields?: AdditionalFieldsPermissionExpression
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export interface Params {
|
|
@@ -33,6 +33,6 @@ export const evaluateTopLevelPermissionsFn = async (
|
|
|
33
33
|
|
|
34
34
|
export const checkFieldsPropertyExists = ({ role }: MachineContext) => {
|
|
35
35
|
const hasFields = !!Object.keys(role?.fields ?? {}).length
|
|
36
|
-
const hasAdditional =
|
|
36
|
+
const hasAdditional = typeof role?.additional_fields !== 'undefined'
|
|
37
37
|
return hasFields || hasAdditional
|
|
38
38
|
}
|