This commit is contained in:
parent
756603bae1
commit
fc6dcaf87a
@ -1,15 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { rolesFromPayload } from './auth.js'
|
||||
import { hasAccess, rolesFromPayload } from './auth.js'
|
||||
|
||||
describe('role extraction', () => {
|
||||
it('collects FieldLens resource and realm roles without treating authentication as authorization', () => {
|
||||
describe('FieldLens authorization isolation', () => {
|
||||
it('extracts FieldLens client roles and ignores realm/platform roles', () => {
|
||||
const roles = rolesFromPayload({
|
||||
resource_access: {
|
||||
'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) {
|
||||
const resourceAccess = payload.resource_access as Record<string, { roles?: unknown }> | undefined
|
||||
const realmAccess = payload.realm_access as { roles?: unknown } | undefined
|
||||
return [...new Set([
|
||||
...strings(resourceAccess?.[config.audience]?.roles),
|
||||
...strings(resourceAccess?.[config.browserClientId]?.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[]> = {
|
||||
operator: ['fieldlens-operator', 'fieldlens-supervisor', 'fieldlens-admin', 'operator', 'admin', 'owner'],
|
||||
supervisor: ['fieldlens-supervisor', 'fieldlens-admin', 'admin', 'owner'],
|
||||
admin: ['fieldlens-admin', 'admin', 'owner'],
|
||||
integration: ['fieldlens-integration', 'fieldlens-admin', 'admin', 'owner'],
|
||||
const access = {
|
||||
operator: ['fieldlens-operator', 'fieldlens-supervisor', 'fieldlens-admin'],
|
||||
supervisor: ['fieldlens-supervisor', 'fieldlens-admin'],
|
||||
admin: ['fieldlens-admin'],
|
||||
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) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const roles = req.identity?.roles ?? []
|
||||
const exactMachineIdentity = level === 'integration' && req.identity?.clientId === config.machineClientId
|
||||
if (!exactMachineIdentity && !access[level].some((role) => roles.includes(role))) {
|
||||
if (!hasAccess(level, req.identity)) {
|
||||
res.status(403).json({ error: 'forbidden' })
|
||||
return
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user