feat(auth): WorkflowRoles configurable claim path + ordinis:admin super-role
User report: token has roles в resource_access.ordinis.roles (client-scoped),
backend WorkflowRoles читал hardcoded realm_access.roles → forbidden_role
для всех reviewer actions.
Backend:
- WorkflowRoles: depends OrdinisAuthProperties → reads via rolesClaim path
(же что ScopeContext). Default realm_access.roles, на staging/prod env
var ORDINIS_AUTH_ROLES_CLAIM=resource_access.ordinis.roles (уже wired
в helm helper).
- Nested claim path resolver — mirror ScopeContext.readClaimByPath
- New super-role ADMIN ('ordinis:admin') — holder автоматически проходит
любой workflow check без явного назначения отдельных ролей. Convenience
для small teams.
Frontend (usePermissions.ts):
- tokenRoles() читает оба claim path (realm_access.roles +
resource_access.ordinis.roles) и объединяет — UI gate работает
независимо от backend config
- hasRole() recognizes 'ordinis:admin' как super-role override
- ROLE_ADMIN exported
Docs:
- docs/operator/rbac.md: добавлен ordinis:admin row, JWT claim path note,
Admin profile updated (super-role vs composite explanation)
This commit is contained in:
@@ -33,19 +33,42 @@ import { useAuth } from 'react-oidc-context'
|
||||
export const ROLE_SCHEMA_REVIEWER = 'ordinis:schema:reviewer'
|
||||
export const ROLE_SCHEMA_PUBLISHER = 'ordinis:schema:publisher'
|
||||
export const ROLE_RECORD_REVIEWER = 'ordinis:record:reviewer'
|
||||
export const ROLE_ADMIN = 'ordinis:admin'
|
||||
|
||||
/** Достаёт realm roles из JWT (Keycloak claim path: realm_access.roles). */
|
||||
function realmRoles(profile: unknown): string[] {
|
||||
/**
|
||||
* Достаёт roles из JWT. Поддерживает оба claim path:
|
||||
* - {@code realm_access.roles} — Keycloak realm-роли
|
||||
* - {@code resource_access.ordinis.roles} — Keycloak client-роли
|
||||
*
|
||||
* <p>Backend читает один из двух через {@code OrdinisAuthProperties.rolesClaim}
|
||||
* (default {@code realm_access.roles}, на prod/staging
|
||||
* {@code resource_access.ordinis.roles}). Frontend читает оба и объединяет
|
||||
* — UI gate должен работать независимо от config'а.
|
||||
*/
|
||||
function tokenRoles(profile: unknown): string[] {
|
||||
if (!profile || typeof profile !== 'object') return []
|
||||
const realmAccess = (profile as { realm_access?: { roles?: unknown } }).realm_access
|
||||
if (!realmAccess || typeof realmAccess !== 'object') return []
|
||||
const roles = (realmAccess as { roles?: unknown }).roles
|
||||
if (!Array.isArray(roles)) return []
|
||||
return roles.filter((r): r is string => typeof r === 'string')
|
||||
const p = profile as {
|
||||
realm_access?: { roles?: unknown }
|
||||
resource_access?: { ordinis?: { roles?: unknown } }
|
||||
}
|
||||
const fromRealm = Array.isArray(p.realm_access?.roles)
|
||||
? (p.realm_access.roles as unknown[]).filter((r): r is string => typeof r === 'string')
|
||||
: []
|
||||
const fromClient = Array.isArray(p.resource_access?.ordinis?.roles)
|
||||
? (p.resource_access.ordinis.roles as unknown[]).filter(
|
||||
(r): r is string => typeof r === 'string',
|
||||
)
|
||||
: []
|
||||
return [...fromRealm, ...fromClient]
|
||||
}
|
||||
|
||||
/**
|
||||
* True если у actor'а есть указанная role или super-role {@link ROLE_ADMIN}.
|
||||
* Admin grants full workflow access — convenience override для small teams.
|
||||
*/
|
||||
function hasRole(profile: unknown, role: string): boolean {
|
||||
return realmRoles(profile).includes(role)
|
||||
const roles = tokenRoles(profile)
|
||||
return roles.includes(role) || roles.includes(ROLE_ADMIN)
|
||||
}
|
||||
|
||||
/** Может ли актор approve / request_changes / reject schema draft. */
|
||||
|
||||
Reference in New Issue
Block a user