fix(auth): иерархическое расширение scope set в ScopeContext
This commit is contained in:
@@ -93,7 +93,33 @@ public class ScopeContext {
|
||||
processRole(result, part);
|
||||
}
|
||||
}
|
||||
return result.isEmpty() ? EnumSet.of(DataScope.PUBLIC) : result;
|
||||
if (result.isEmpty()) return EnumSet.of(DataScope.PUBLIC);
|
||||
return expandHierarchy(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Иерархическое расширение scope set: RESTRICTED видит INTERNAL+PUBLIC,
|
||||
* INTERNAL видит PUBLIC. {@link DataScope#visibleTo} уже описывает эту
|
||||
* семантику для одиночного scope сравнения; здесь применяем её к set
|
||||
* чтобы все downstream API ({@code RecordReadService.allowedScopes.contains})
|
||||
* работали с уже расширенным набором.
|
||||
*
|
||||
* <p>Без этого юзер с client-ролью только {@code admin} (→ RESTRICTED)
|
||||
* не мог читать PUBLIC справочники: {@code [RESTRICTED].contains(PUBLIC)
|
||||
* = false} → 403 scope_access_denied. Контекст: altum realm присваивает
|
||||
* единственную роль {@code admin} на client {@code ordinis}, в отличие
|
||||
* от nstart realm где исторически назначались 4 роли
|
||||
* (public/internal/restricted/admin) явно.
|
||||
*/
|
||||
private static EnumSet<DataScope> expandHierarchy(EnumSet<DataScope> base) {
|
||||
EnumSet<DataScope> expanded = EnumSet.copyOf(base);
|
||||
if (base.contains(DataScope.RESTRICTED)) {
|
||||
expanded.add(DataScope.INTERNAL);
|
||||
expanded.add(DataScope.PUBLIC);
|
||||
} else if (base.contains(DataScope.INTERNAL)) {
|
||||
expanded.add(DataScope.PUBLIC);
|
||||
}
|
||||
return expanded;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+39
-14
@@ -39,9 +39,13 @@ class ScopeContextTest {
|
||||
|
||||
@Test
|
||||
void mapsUnderscoreUppercaseRolesToScopes() {
|
||||
// RESTRICTED-яльная роль теперь иерархически расширяется в
|
||||
// PUBLIC+INTERNAL+RESTRICTED (см. expandHierarchy в ScopeContext).
|
||||
// Иначе RESTRICTED user не мог бы читать PUBLIC справочники
|
||||
// (RecordReadService использует List.contains, не visibleTo).
|
||||
setJwtWithRealmRoles(List.of("ORDINIS_RESTRICTED"));
|
||||
assertThat(new ScopeContext(propsNoQuery).currentScopes())
|
||||
.containsExactly(DataScope.RESTRICTED);
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -71,9 +75,10 @@ class ScopeContextTest {
|
||||
|
||||
@Test
|
||||
void mapsMixedColonAndDashRoles() {
|
||||
// RESTRICTED → expand добавляет INTERNAL+PUBLIC. PUBLIC уже был.
|
||||
setJwtWithRealmRoles(List.of("ordinis:client:public", "ordinis-restricted"));
|
||||
assertThat(new ScopeContext(propsNoQuery).currentScopes())
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.RESTRICTED);
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,36 +90,55 @@ class ScopeContextTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminRoleGetsRestricted() {
|
||||
void adminRoleGetsAllScopes() {
|
||||
// Любая роль с tail'ом ADMIN (admin / ordinis-admin / ADMIN_FOO с
|
||||
// суффиксом ADMIN) даёт RESTRICTED — RESTRICTED visibleTo всем уровням.
|
||||
// Раньше admin молча проваливался в normalizeRoleToScope → PUBLIC →
|
||||
// админ получал 403 на /admin/* endpoint'ах с requireInternal().
|
||||
// суффиксом ADMIN) даёт RESTRICTED — а через expandHierarchy и
|
||||
// INTERNAL + PUBLIC. Раньше admin молча проваливался в
|
||||
// normalizeRoleToScope → PUBLIC → админ получал 403 на /admin/*
|
||||
// endpoint'ах с requireInternal(). Сейчас admin получает все три
|
||||
// scope'а, может читать PUBLIC/INTERNAL/RESTRICTED справочники.
|
||||
setJwtWithRealmRoles(List.of("admin"));
|
||||
assertThat(new ScopeContext(propsNoQuery).currentScopes())
|
||||
.containsExactly(DataScope.RESTRICTED);
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordinisDashAdminGetsRestricted() {
|
||||
void ordinisDashAdminGetsAllScopes() {
|
||||
setJwtWithRealmRoles(List.of("ordinis-admin"));
|
||||
assertThat(new ScopeContext(propsNoQuery).currentScopes())
|
||||
.containsExactly(DataScope.RESTRICTED);
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminColonSeparatedGetsRestricted() {
|
||||
void adminColonSeparatedGetsAllScopes() {
|
||||
setJwtWithRealmRoles(List.of("ordinis:client:admin"));
|
||||
assertThat(new ScopeContext(propsNoQuery).currentScopes())
|
||||
.containsExactly(DataScope.RESTRICTED);
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminAlongsideExplicitScopesUnionizes() {
|
||||
// admin + ordinis-public → RESTRICTED (admin) + PUBLIC (ordinis-public).
|
||||
// admin → RESTRICTED → expand до всех трёх. Дополнительный
|
||||
// ordinis-public ничего не добавляет (уже в наборе).
|
||||
setJwtWithRealmRoles(List.of("admin", "ordinis-public"));
|
||||
assertThat(new ScopeContext(propsNoQuery).currentScopes())
|
||||
.containsExactlyInAnyOrder(DataScope.RESTRICTED, DataScope.PUBLIC);
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void internalOnlyExpandsToPublic() {
|
||||
// INTERNAL role → INTERNAL + auto PUBLIC. PUBLIC справочники видны.
|
||||
setJwtWithRealmRoles(List.of("ordinis-internal"));
|
||||
assertThat(new ScopeContext(propsNoQuery).currentScopes())
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicOnlyStaysPublic() {
|
||||
// PUBLIC role не расширяется — нечего расширять (он самый широкий снизу).
|
||||
setJwtWithRealmRoles(List.of("ordinis-public"));
|
||||
assertThat(new ScopeContext(propsNoQuery).currentScopes())
|
||||
.containsExactly(DataScope.PUBLIC);
|
||||
}
|
||||
|
||||
// ============= JWT vs query =============
|
||||
@@ -151,10 +175,11 @@ class ScopeContextTest {
|
||||
|
||||
@Test
|
||||
void supportsCustomClaimPath() {
|
||||
// RESTRICTED → expand добавляет INTERNAL (PUBLIC уже был).
|
||||
var props = new OrdinisAuthProperties(null, null, null, "scopes", false, false);
|
||||
setJwtWithFlatScopes(List.of("PUBLIC", "RESTRICTED"));
|
||||
assertThat(new ScopeContext(props).currentScopes())
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.RESTRICTED);
|
||||
.containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user