This commit is contained in:
parent
756603bae1
commit
fc6dcaf87a
@ -1,15 +1,35 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { rolesFromPayload } from './auth.js'
|
import { hasAccess, rolesFromPayload } from './auth.js'
|
||||||
|
|
||||||
describe('role extraction', () => {
|
describe('FieldLens authorization isolation', () => {
|
||||||
it('collects FieldLens resource and realm roles without treating authentication as authorization', () => {
|
it('extracts FieldLens client roles and ignores realm/platform roles', () => {
|
||||||
const roles = rolesFromPayload({
|
const roles = rolesFromPayload({
|
||||||
resource_access: {
|
resource_access: {
|
||||||
'fieldlens-api': { roles: ['fieldlens-operator'] },
|
'fieldlens-api': { roles: ['fieldlens-operator'] },
|
||||||
},
|
},
|
||||||
realm_access: { roles: ['offline_access'] },
|
realm_access: { roles: ['operator', 'admin', 'owner', 'offline_access'] },
|
||||||
})
|
})
|
||||||
expect(roles).toContain('fieldlens-operator')
|
|
||||||
expect(roles).toContain('offline_access')
|
expect(roles).toEqual(['fieldlens-operator'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not grant FieldLens access from generic Arrowstack roles', () => {
|
||||||
|
const platformIdentity = {
|
||||||
|
roles: ['operator', 'admin', 'owner'],
|
||||||
|
clientId: 'some-arrowstack-client',
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(hasAccess('operator', platformIdentity)).toBe(false)
|
||||||
|
expect(hasAccess('supervisor', platformIdentity)).toBe(false)
|
||||||
|
expect(hasAccess('admin', platformIdentity)).toBe(false)
|
||||||
|
expect(hasAccess('integration', platformIdentity)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves the FieldLens role hierarchy', () => {
|
||||||
|
expect(hasAccess('operator', { roles: ['fieldlens-operator'] })).toBe(true)
|
||||||
|
expect(hasAccess('operator', { roles: ['fieldlens-supervisor'] })).toBe(true)
|
||||||
|
expect(hasAccess('supervisor', { roles: ['fieldlens-admin'] })).toBe(true)
|
||||||
|
expect(hasAccess('admin', { roles: ['fieldlens-admin'] })).toBe(true)
|
||||||
|
expect(hasAccess('admin', { roles: ['fieldlens-supervisor'] })).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -27,12 +27,10 @@ function strings(value: unknown) {
|
|||||||
|
|
||||||
export function rolesFromPayload(payload: JWTPayload) {
|
export function rolesFromPayload(payload: JWTPayload) {
|
||||||
const resourceAccess = payload.resource_access as Record<string, { roles?: unknown }> | undefined
|
const resourceAccess = payload.resource_access as Record<string, { roles?: unknown }> | undefined
|
||||||
const realmAccess = payload.realm_access as { roles?: unknown } | undefined
|
|
||||||
return [...new Set([
|
return [...new Set([
|
||||||
...strings(resourceAccess?.[config.audience]?.roles),
|
...strings(resourceAccess?.[config.audience]?.roles),
|
||||||
...strings(resourceAccess?.[config.browserClientId]?.roles),
|
...strings(resourceAccess?.[config.browserClientId]?.roles),
|
||||||
...strings(resourceAccess?.[config.machineClientId]?.roles),
|
...strings(resourceAccess?.[config.machineClientId]?.roles),
|
||||||
...strings(realmAccess?.roles),
|
|
||||||
])]
|
])]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,18 +68,24 @@ export async function authenticate(req: Request, res: Response, next: NextFuncti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const access: Record<string, string[]> = {
|
const access = {
|
||||||
operator: ['fieldlens-operator', 'fieldlens-supervisor', 'fieldlens-admin', 'operator', 'admin', 'owner'],
|
operator: ['fieldlens-operator', 'fieldlens-supervisor', 'fieldlens-admin'],
|
||||||
supervisor: ['fieldlens-supervisor', 'fieldlens-admin', 'admin', 'owner'],
|
supervisor: ['fieldlens-supervisor', 'fieldlens-admin'],
|
||||||
admin: ['fieldlens-admin', 'admin', 'owner'],
|
admin: ['fieldlens-admin'],
|
||||||
integration: ['fieldlens-integration', 'fieldlens-admin', 'admin', 'owner'],
|
integration: ['fieldlens-integration', 'fieldlens-admin'],
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function hasAccess(
|
||||||
|
level: keyof typeof access,
|
||||||
|
identity: Pick<AuthIdentity, 'roles' | 'clientId'> | undefined,
|
||||||
|
) {
|
||||||
|
const exactMachineIdentity = level === 'integration' && identity?.clientId === config.machineClientId
|
||||||
|
return exactMachineIdentity || access[level].some((role) => identity?.roles.includes(role) ?? false)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireAccess(level: keyof typeof access) {
|
export function requireAccess(level: keyof typeof access) {
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
const roles = req.identity?.roles ?? []
|
if (!hasAccess(level, req.identity)) {
|
||||||
const exactMachineIdentity = level === 'integration' && req.identity?.clientId === config.machineClientId
|
|
||||||
if (!exactMachineIdentity && !access[level].some((role) => roles.includes(role))) {
|
|
||||||
res.status(403).json({ error: 'forbidden' })
|
res.status(403).json({ error: 'forbidden' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user