fix(auth): scope-фильтр на writer-side endpoints (Phase 2c security gap)
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 зелёные.
This commit is contained in:
@@ -113,16 +113,13 @@ class AuthE2ETest {
|
||||
.content(om.writeValueAsBytes(recBody)))
|
||||
.andExpect(status().is2xxSuccessful());
|
||||
|
||||
// ⚠️ ИЗВЕСТНОЕ ПОВЕДЕНИЕ: writer-side endpoint /api/v1/dictionaries/{n}/records/{k}
|
||||
// НЕ фильтрует по scope юзера. Public-юзер сейчас МОЖЕТ прочитать INTERNAL.
|
||||
// Read-api (/api/v1/{n}/records/...) — фильтрует. Writer считается admin-only.
|
||||
//
|
||||
// Тест документирует текущее поведение. После security review (см. issue
|
||||
// #scope-writer-filter) заменить на 403/404 expectation.
|
||||
// Writer endpoint фильтрует по scope: PUBLIC-юзер не видит INTERNAL словарь.
|
||||
// Возвращаем 404 (а не 403) — намеренно скрываем существование INTERNAL
|
||||
// данных, как делает read-api. Защита от enumeration.
|
||||
String publicToken = JwtTestSupport.signJwt(List.of("ordinis:client:public"), "user-public");
|
||||
mvc.perform(get("/api/v1/dictionaries/{n}/records/{k}", dictName, key)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + publicToken))
|
||||
.andExpect(status().isOk()); // TODO: должно быть isForbidden() после fix scope filter
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user