ScopeContext.normalizeRoleToScope ищет суффикс PUBLIC|INTERNAL|RESTRICTED
после `-`/`_`/`:`. Роль `admin` (и `ordinis-admin`, `ordinis:client:admin`)
не match'илась — нет суффикса с known scope name → DataScope.valueOf("ADMIN")
бросал IllegalArgumentException → ignored → юзер с одной только admin
ролью получал PUBLIC scope → 403 на любом /admin/* endpoint'е с requireInternal().
Симптом на проде/staging: админ открывает /audit, видит 403 на /admin/audit
и /admin/users несмотря на JWT с admin ролью.
Fix: processRole() сначала проверяет isAdminRole (tail == "ADMIN" после
prefix-stripping и separator-split) → даёт RESTRICTED (RESTRICTED visibleTo
PUBLIC + INTERNAL + RESTRICTED). Иначе идёт обычный normalizeRoleToScope.
Поддерживаются: admin, ADMIN, ordinis-admin, ordinis_admin, ordinis:client:admin,
ANYTHING-admin, ANYTHING_ADMIN — короче, любая роль с tail'ом ADMIN.
Tests: +4 новых кейса (admin alone, ordinis-dash-admin, colon-separated admin,
admin + explicit scope unionize). Старый unrecognizedRolesGivePublicFallback
оставил только "viewer" — admin теперь recognized.
17/17 ScopeContextTest passed.
ScopeContext.canAccess(DataScope) — единая точка проверки доступа,
использует DataScope.visibleTo (PUBLIC видит PUBLIC, INTERNAL видит
PUBLIC+INTERNAL, RESTRICTED видит всё).
DictionaryRecordController:
- requireReadAccess на GET /{businessKey} и /{businessKey}/history
→ 404 dictionary_not_found если scope словаря недоступен (намеренно
скрываем существование INTERNAL/RESTRICTED данных, защита от
enumeration)
- requireWriteAccess на POST/PUT/DELETE
→ 403 scope_insufficient если scope недостаточен для записи
DictionaryDefinitionController:
- list() фильтрует список по canAccess (PUBLIC-юзер не видит INTERNAL
словари в каталоге)
- get() → 404 если недоступен
- create()/update() → 403 если новый scope выше доступа юзера
(защита от scope escalation через PUT)
AuthE2ETest.publicUser_cannotSeeInternalRecords: TODO snять, expectation
обновлён на isNotFound() (раньше было isOk + комментарий о gap).
До этого fix: PUBLIC-юзер мог GET /api/v1/dictionaries/{n}/records/{k}
INTERNAL-словарь на writer-side. Read-api (отдельный модуль) фильтровал
корректно. Issue найден через AuthE2ETest в Phase 2c.
Все 6 AuthE2ETest и 24 unit-теста rest-api зелёные.
Найден production-bug на staging: ordinis-app в CrashLoopBackOff с
NoClassDefFoundError: com/nimbusds/jose/RemoteKeySourceException.
Причина: в Phase 2c e2e тестах я добавил nimbus-jose-jwt 9.37.3 как
test-scope direct dependency в ordinis-app/pom.xml. Maven scope
resolution: direct test-scope побеждает transitive compile-scope,
nimbus исключился из Spring Boot fat jar. На runtime
JwtDecoders.fromIssuerLocation() (использует Nimbus internally) падал.
Fix:
- ordinis-auth/pom.xml: explicit compile-scope dep на nimbus-jose-jwt
(раньше тянулся транзитивно через oauth2-resource-server starter)
- ordinis-app/pom.xml: убрал test-scope direct decl, JwtTestSupport
использует nimbus с compile classpath.
Verified: BOOT-INF/lib/nimbus-jose-jwt-9.37.3.jar в fat jar после rebuild.
e2e AuthE2ETest всё ещё зелёный (12/12).
Альтум выдаёт роли формата "ordinis:client:public" / "ordinis:client:internal" /
"ordinis:client:restricted" в realm_access.roles. Старая версия ScopeContext
обрабатывала только "-" / "_" разделители + ORDINIS- префикс — colon-роли
приводили к fallback на PUBLIC.
Теперь normalizeRoleToScope:
- strips "ORDINIS:" (как и "ORDINIS-" / "ORDINIS_")
- берёт сегмент после последнего из "-", "_", ":"
Совместимо с прежними форматами. + 2 unit-теста (всего 13 в ScopeContextTest).
Новый модуль ordinis-auth:
- OrdinisAuthProperties — public-key, issuer, requireAuthentication, allowQueryScope
- ScopeContext — резолвит current scopes из JWT claim 'scopes' (приоритет),
потом legacy ?as_scope=, потом anonymous=PUBLIC
- SecurityConfig — Spring Security OAuth2 Resource Server + RSA public-key
decoder. Если public-key пуст — JWT validation отключён (permissive
fallback); rollout без auth-сервера не ломает API.
Read controller теперь делегирует scope resolution в ScopeContext вместо
прямого парсинга ?as_scope=. Backward compat: legacy query параметр
работает пока ordinis.auth.allow-query-scope=true (по умолчанию true в
dev/staging, false в prod после интеграции с @nstart/auth).
Vault: public-key читается из secret/ordinis/cuod/jwt-public-key (key=key).
Spring Cloud Vault flatten'ит в property 'key', resolved в
ordinis.auth.public-key=${key:}.