Skip to content

Commit 45ded56

Browse files
fix: roles read & write
1 parent f7a3b13 commit 45ded56

File tree

10 files changed

+112
-65
lines changed

10 files changed

+112
-65
lines changed

packages/flowerbase/src/features/triggers/__tests__/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ describe('activateTriggers', () => {
2121

2222
it('skips triggers marked as disabled', async () => {
2323
const functionsList = {
24-
runEnabled: jest.fn(),
25-
runDisabled: jest.fn()
24+
runEnabled: { code: 'exports = async () => {}' },
25+
runDisabled: { code: 'exports = async () => {}' }
2626
}
2727

2828
await activateTriggers({

packages/flowerbase/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ describe('mongodb-atlas findOneAndUpdate', () => {
171171

172172
const app = createAppWithCollection(collection)
173173
const operators = MongoDbAtlas(app as any, {
174-
rules: createRules({ insert: false }),
174+
rules: createRules({ write: undefined, insert: false }),
175175
user: { id: 'user-1' }
176176
})
177177
.db('db')

packages/flowerbase/src/utils/__tests__/STEP_C_STATES.test.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@ describe('STEP_C_STATES', () => {
8989
})
9090
mockedLogInfo.mockRestore()
9191
})
92-
it('evaluateTopLevelWrite should end a success validation when write is true and no field rules exist', async () => {
92+
it('evaluateTopLevelWrite should end a success validation when read is true', async () => {
9393
const mockedLogInfo = jest
9494
.spyOn(Utils, 'logMachineInfo')
9595
.mockImplementation(() => 'Mocked Value')
96-
;(evaluateTopLevelWriteFn as jest.Mock).mockReturnValue(true)
96+
;(evaluateTopLevelWriteFn as jest.Mock).mockReturnValue(false)
9797
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(false)
98-
const mockContext = { prevParams: { readCheck: false } } as unknown as MachineContext
98+
const mockContext = { prevParams: { readCheck: true } } as unknown as MachineContext
9999
await evaluateTopLevelWrite({
100100
endValidation,
101101
context: mockContext,
@@ -112,9 +112,8 @@ describe('STEP_C_STATES', () => {
112112
})
113113
mockedLogInfo.mockRestore()
114114
})
115-
it('evaluateTopLevelWrite should go to checkFieldsProperty when write is true and field rules exist', async () => {
115+
it('evaluateTopLevelWrite should end a success validation when read is false and write is true', async () => {
116116
;(evaluateTopLevelWriteFn as jest.Mock).mockReturnValue(true)
117-
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
118117
const mockContext = { prevParams: { readCheck: false } } as unknown as MachineContext
119118
await evaluateTopLevelWrite({
120119
endValidation,
@@ -123,13 +122,13 @@ describe('STEP_C_STATES', () => {
123122
next,
124123
initialStep: null
125124
})
126-
expect(next).toHaveBeenCalledWith('checkFieldsProperty')
125+
expect(endValidation).toHaveBeenCalledWith({ success: true })
127126
})
128-
it('evaluateTopLevelWrite should end a failed validation if both read and write are not true', async () => {
127+
it('evaluateTopLevelWrite should end a failed validation when read is false and write is not true', async () => {
129128
(evaluateTopLevelWriteFn as jest.Mock).mockReturnValue(false)
130129
const mockContext = {
131130
prevParams: {
132-
readCheck: undefined
131+
readCheck: false
133132
}
134133
} as unknown as MachineContext
135134
await evaluateTopLevelWrite({
@@ -141,12 +140,45 @@ describe('STEP_C_STATES', () => {
141140
})
142141
expect(endValidation).toHaveBeenCalledWith({ success: false })
143142
})
144-
it('evaluateTopLevelWrite should allow through readCheck=true even if write=false', async () => {
143+
it('evaluateTopLevelWrite should end a success validation when read is undefined and write is true', async () => {
144+
;(evaluateTopLevelWriteFn as jest.Mock).mockReturnValue(true)
145+
const mockContext = {
146+
prevParams: {
147+
readCheck: undefined
148+
}
149+
} as unknown as MachineContext
150+
await evaluateTopLevelWrite({
151+
endValidation,
152+
context: mockContext,
153+
goToNextValidationStage,
154+
next,
155+
initialStep: null
156+
})
157+
expect(endValidation).toHaveBeenCalledWith({ success: true })
158+
})
159+
it('evaluateTopLevelWrite should go to checkFieldsProperty when read and write are undefined/false but fields exist', async () => {
145160
(evaluateTopLevelWriteFn as jest.Mock).mockReturnValue(false)
161+
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
162+
const mockContext = {
163+
prevParams: {
164+
readCheck: undefined
165+
}
166+
} as unknown as MachineContext
167+
await evaluateTopLevelWrite({
168+
endValidation,
169+
context: mockContext,
170+
goToNextValidationStage,
171+
next,
172+
initialStep: null
173+
})
174+
expect(next).toHaveBeenCalledWith('checkFieldsProperty')
175+
})
176+
it('evaluateTopLevelWrite should end a failed validation when read and write are undefined/false and no field rules exist', async () => {
177+
;(evaluateTopLevelWriteFn as jest.Mock).mockReturnValue(undefined)
146178
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(false)
147179
const mockContext = {
148180
prevParams: {
149-
readCheck: true
181+
readCheck: undefined
150182
}
151183
} as unknown as MachineContext
152184
await evaluateTopLevelWrite({
@@ -156,6 +188,6 @@ describe('STEP_C_STATES', () => {
156188
next,
157189
initialStep: null
158190
})
159-
expect(endValidation).toHaveBeenCalledWith({ success: true })
191+
expect(endValidation).toHaveBeenCalledWith({ success: false })
160192
})
161193
})

packages/flowerbase/src/utils/__tests__/WRITE_STEP_B_STATES.test.ts

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,8 @@ describe('WRITE STEP_B_STATES', () => {
7777
expect(endValidation).toHaveBeenCalledWith({ success: false })
7878
})
7979

80-
it('allows write when write=true and no field-level rules are defined', async () => {
80+
it('routes to insert check when write=true', async () => {
8181
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
82-
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(false)
8382
const context = {} as MachineContext
8483
await evaluateTopLevelWrite({
8584
context,
@@ -88,10 +87,10 @@ describe('WRITE STEP_B_STATES', () => {
8887
next,
8988
initialStep: null
9089
})
91-
expect(endValidation).toHaveBeenCalledWith({ success: true })
90+
expect(next).toHaveBeenCalledWith('evaluateTopLevelInsert')
9291
})
9392

94-
it('routes to field-level checks when write=true and field-level rules exist', async () => {
93+
it('routes to insert check when write=true and field-level rules exist', async () => {
9594
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
9695
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
9796
const context = {} as MachineContext
@@ -102,7 +101,7 @@ describe('WRITE STEP_B_STATES', () => {
102101
next,
103102
initialStep: null
104103
})
105-
expect(next).toHaveBeenCalledWith('checkFieldsProperty')
104+
expect(next).toHaveBeenCalledWith('evaluateTopLevelInsert')
106105
})
107106

108107
it('denies when write=false', async () => {
@@ -118,7 +117,7 @@ describe('WRITE STEP_B_STATES', () => {
118117
expect(endValidation).toHaveBeenCalledWith({ success: false })
119118
})
120119

121-
it('routes to insert check when write is undefined', async () => {
120+
it('routes to field-level checks when write is undefined', async () => {
122121
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(undefined)
123122
const context = {} as MachineContext
124123
await evaluateTopLevelWrite({
@@ -128,7 +127,7 @@ describe('WRITE STEP_B_STATES', () => {
128127
next,
129128
initialStep: null
130129
})
131-
expect(next).toHaveBeenCalledWith('evaluateTopLevelInsert')
130+
expect(next).toHaveBeenCalledWith('checkFieldsProperty')
132131
})
133132

134133
it('denies insert when insert is false/undefined', async () => {
@@ -144,9 +143,8 @@ describe('WRITE STEP_B_STATES', () => {
144143
expect(endValidation).toHaveBeenCalledWith({ success: false })
145144
})
146145

147-
it('allows insert when insert=true and no field-level rules are defined', async () => {
146+
it('allows when insert=true', async () => {
148147
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
149-
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(false)
150148
const context = {} as MachineContext
151149
await evaluateTopLevelInsert({
152150
context,
@@ -158,20 +156,6 @@ describe('WRITE STEP_B_STATES', () => {
158156
expect(endValidation).toHaveBeenCalledWith({ success: true })
159157
})
160158

161-
it('routes insert to field-level checks when rules exist', async () => {
162-
;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
163-
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
164-
const context = {} as MachineContext
165-
await evaluateTopLevelInsert({
166-
context,
167-
endValidation,
168-
goToNextValidationStage,
169-
next,
170-
initialStep: null
171-
})
172-
expect(next).toHaveBeenCalledWith('checkFieldsProperty')
173-
})
174-
175159
it('routes checkFieldsProperty to checkIsValidFieldName when field rules exist', async () => {
176160
;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
177161
const context = {} as MachineContext

packages/flowerbase/src/utils/roles/helpers.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,38 @@ import { MachineContext } from './machines/interface'
88

99
const functionsConditions = ['%%true', '%%false']
1010

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+
1123
export const evaluateExpression = async (
1224
params: MachineContext['params'],
1325
expression?: PermissionExpression,
1426
user?: MachineContext['user']
1527
): Promise<boolean> => {
1628
if (!expression || typeof expression === 'boolean') return !!expression
29+
const normalizedUser = normalizeUserRole(user)
1730

1831
const value = {
1932
...params.expansions,
2033
...params.cursor,
21-
'%%user': user,
34+
'%%user': normalizedUser,
2235
'%%true': true
2336
}
2437
const conditions = expandQuery(expression, value)
2538
const complexCondition = Object.entries(conditions as Record<string, any>).find(([key]) =>
2639
functionsConditions.includes(key)
2740
)
2841
return complexCondition
29-
? await evaluateComplexExpression(complexCondition, params, user)
42+
? await evaluateComplexExpression(complexCondition, params, normalizedUser)
3043
: rulesMatcherUtils.checkRule(conditions, value, {})
3144
}
3245

@@ -36,6 +49,7 @@ const evaluateComplexExpression = async (
3649
user: MachineContext['user']
3750
): Promise<boolean> => {
3851
const [key, config] = condition
52+
const normalizedUser = normalizeUserRole(user)
3953

4054
const functionConfig = config['%function']
4155
const { name, arguments: fnArguments } = functionConfig
@@ -47,7 +61,7 @@ const evaluateComplexExpression = async (
4761
...params.expansions,
4862
...params.cursor,
4963
'%%root': params.cursor,
50-
'%%user': user,
64+
'%%user': normalizedUser,
5165
'%%true': true,
5266
'%%false': false
5367
}
@@ -62,7 +76,7 @@ const evaluateComplexExpression = async (
6276
args: expandedArguments,
6377
app,
6478
rules: StateManager.select("rules"),
65-
user,
79+
user: normalizedUser,
6680
currentFunction,
6781
functionName: name,
6882
functionsList,

packages/flowerbase/src/utils/roles/machines/fieldPermissions.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ export const hasAdditionalFieldsDefined = (role?: Role) =>
5454

5555
export const filterDocumentByFieldPermissions = async (
5656
context: Pick<MachineContext, 'params' | 'role' | 'user'>,
57-
mode: 'read' | 'write'
57+
mode: 'read' | 'write',
58+
options?: {
59+
defaultAllow?: boolean
60+
}
5861
): Promise<Document> => {
5962
const source = context.params?.cursor
6063
if (!isObject(source)) return {}
@@ -66,12 +69,13 @@ export const filterDocumentByFieldPermissions = async (
6669
for (const [key, value] of Object.entries(source)) {
6770
const fieldPermission = fields[key]
6871
const permission = fieldPermission ?? getAdditionalFieldPermission(additionalFields, key)
69-
if (!permission) continue
70-
71-
const allowed =
72-
mode === 'read'
73-
? await canReadField(context, permission)
74-
: await canWriteField(context, permission)
72+
let allowed = options?.defaultAllow === true
73+
if (permission) {
74+
allowed =
75+
mode === 'read'
76+
? await canReadField(context, permission)
77+
: await canWriteField(context, permission)
78+
}
7579

7680
if (allowed) {
7781
document[key] = value

packages/flowerbase/src/utils/roles/machines/read/C/index.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { States } from '../../interface'
2+
import { logMachineInfo } from '../../utils'
13
import {
24
checkFieldsPropertyExists,
35
evaluateTopLevelReadFn,
46
evaluateTopLevelWriteFn
57
} from './validators'
6-
import { States } from '../../interface'
7-
import { logMachineInfo } from '../../utils'
88

99
export const STEP_C_STATES: States = {
10-
evaluateTopLevelRead: async ({ context, next, endValidation }) => {
10+
evaluateTopLevelRead: async ({ context, next }) => {
1111
logMachineInfo({
1212
enabled: context.enableLog,
1313
machine: 'C',
@@ -25,11 +25,18 @@ export const STEP_C_STATES: States = {
2525
stepName: 'evaluateTopLevelWrite'
2626
})
2727
const writeCheck = await evaluateTopLevelWriteFn(context)
28-
const readCheck = context?.prevParams?.readCheck === true
29-
if (!readCheck && !writeCheck) return endValidation({ success: false })
28+
const readCheck = context?.prevParams?.readCheck
29+
30+
if (readCheck === true || writeCheck === true) {
31+
return checkFieldsPropertyExists(context)
32+
? next('checkFieldsProperty')
33+
: endValidation({ success: true })
34+
}
35+
36+
if (readCheck === false) return endValidation({ success: false })
3037
return checkFieldsPropertyExists(context)
3138
? next('checkFieldsProperty')
32-
: endValidation({ success: true })
39+
: endValidation({ success: false })
3340
},
3441
checkFieldsProperty: async ({ context, goToNextValidationStage }) => {
3542
logMachineInfo({

packages/flowerbase/src/utils/roles/machines/read/D/validators.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { evaluateTopLevelPermissionsFn } from '../../commonValidators'
12
import {
23
filterDocumentByFieldPermissions,
34
hasAdditionalFieldsDefined
@@ -8,5 +9,11 @@ export const checkAdditionalFieldsFn = ({ role }: MachineContext) => {
89
return hasAdditionalFieldsDefined(role)
910
}
1011

11-
export const checkIsValidFieldNameFn = async (context: MachineContext) =>
12-
await filterDocumentByFieldPermissions(context, 'read')
12+
export const checkIsValidFieldNameFn = async (context: MachineContext) => {
13+
const readCheck = await evaluateTopLevelPermissionsFn(context, 'read')
14+
const writeCheck = await evaluateTopLevelPermissionsFn(context, 'write')
15+
16+
return await filterDocumentByFieldPermissions(context, 'read', {
17+
defaultAllow: readCheck === true || writeCheck === true
18+
})
19+
}

packages/flowerbase/src/utils/roles/machines/write/B/index.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,12 @@ export const STEP_B_STATES: States = {
3737
})
3838
const check = await evaluateTopLevelPermissionsFn(context, 'write')
3939
if (check) {
40-
return checkFieldsPropertyExists(context)
41-
? next('checkFieldsProperty')
42-
: endValidation({ success: true })
40+
return next('evaluateTopLevelInsert')
4341
}
4442
if (check === false) {
4543
return endValidation({ success: false })
4644
}
47-
return next('evaluateTopLevelInsert')
45+
return next('checkFieldsProperty')
4846
},
4947
checkFieldsProperty: async ({ context, goToNextValidationStage }) => {
5048
logMachineInfo({
@@ -69,8 +67,6 @@ export const STEP_B_STATES: States = {
6967
if (!check) {
7068
return endValidation({ success: false })
7169
}
72-
return checkFieldsPropertyExists(context)
73-
? next('checkFieldsProperty')
74-
: endValidation({ success: true })
70+
return endValidation({ success: true })
7571
}
7672
}

packages/flowerbase/src/utils/roles/machines/write/C/validators.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ export const checkAdditionalFieldsFn = ({ role }: MachineContext) => {
88
return hasAdditionalFieldsDefined(role)
99
}
1010

11-
export const checkIsValidFieldNameFn = async (context: MachineContext) =>
12-
await filterDocumentByFieldPermissions(context, 'write')
11+
export const checkIsValidFieldNameFn = async (context: MachineContext) => {
12+
return await filterDocumentByFieldPermissions(context, 'write', {
13+
defaultAllow: typeof context.role.write !== 'undefined'
14+
})
15+
}

0 commit comments

Comments
 (0)