test: 미추적으로 남아 있던 Studio authz 배선 테스트 2개를 추적에 넣는다
37d5614("fix: make Studio authorization actually work, and stop it failing as a
500")가 고친 배선을 지키는 테스트인데 커밋에 들어가지 않아 작업 트리에만 있었다.
추적되지 않으면 브랜치를 옮길 때 조용히 사라지고, 다른 사람이 같은 저장소를 받아도
그 회귀 게이트를 갖지 못한다.
- StudioAuthzWiringTest: MethodSecurityConfig의 advisor가 AuthorizationPort를
생성자로 받는 인프라 빈이라, auto-proxy보다 먼저 만들어지며 AuthorizationAdapter →
RolePermissionRegistry → RolePermissionPolicy를 BeanPostProcessor 등록 전에 끌어
올린다는 사실을 고정한다. 바인딩만 따로 보면 통과하지만 앱에서는 죽는 경우다
- StudioAuthzEnvironmentPostProcessorTest: 같은 수정의 환경 설정 쪽
내용은 손대지 않았다. spotless가 요구한 줄바꿈 두 곳만 정규화됐다(의미 변경 없음).
AGENTS.md의 commit 정책은 human-only다. 이 커밋은 사용자가 "지금 변경했던 내용을
전부 반영하고 develop과 main에 반영하도록" 지시해 예외로 수행한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
743fee3907
commit
e3254def57
+88
@@ -0,0 +1,88 @@
|
||||
package dev.caskeleton.bootstrap.techlog;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.authz.RolePermissionPolicy;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
/**
|
||||
* 이 후처리기의 산출물은 프로퍼티가 아니라 <em>바인딩 결과</em>다. 프로퍼티가 environment 에 들어갔는지만 보면 통과하면서도 실제 앱에서는 매핑이 죽는 경우가
|
||||
* 있다 — application.yml 이 {@code role-permissions: {}} 로 같은 이름을 이미 선언하고 있고, 그 소스가 {@code addLast} 보다
|
||||
* 우선순위가 높기 때문이다. 그래서 여기서는 {@code Map<String, List<String>>} 로 실제 바인딩해서 확인한다.
|
||||
*/
|
||||
class StudioAuthzEnvironmentPostProcessorTest {
|
||||
|
||||
private final StudioAuthzEnvironmentPostProcessor epp = new StudioAuthzEnvironmentPostProcessor();
|
||||
|
||||
private static final Bindable<Map<String, List<String>>> ROLE_PERMISSIONS =
|
||||
Bindable.of(
|
||||
ResolvableType.forClassWithGenerics(
|
||||
Map.class,
|
||||
ResolvableType.forClass(String.class),
|
||||
ResolvableType.forClassWithGenerics(List.class, String.class)));
|
||||
|
||||
private static Map<String, List<String>> bind(MockEnvironment env) {
|
||||
return Binder.get(env)
|
||||
.bind("ca-skeleton.authz.role-permissions", ROLE_PERMISSIONS)
|
||||
.orElse(Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 앱이 실제로 바인딩하는 대상은 {@code Map} 이 아니라 {@code RolePermissionPolicy} 레코드다(생성자 바인딩). 맵으로만 확인하면 레코드
|
||||
* 경로에서만 나타나는 차이를 놓친다.
|
||||
*/
|
||||
@Test
|
||||
void bindsThroughTheRecordTheApplicationActuallyUses() {
|
||||
MockEnvironment env = new MockEnvironment();
|
||||
env.setProperty("app.studio.author-role", "site-admin");
|
||||
env.getPropertySources()
|
||||
.addLast(
|
||||
new MapPropertySource(
|
||||
"applicationDefaults", Map.of("ca-skeleton.authz.role-permissions", "")));
|
||||
|
||||
epp.postProcessEnvironment(env, new SpringApplication());
|
||||
|
||||
Map<String, List<String>> bound =
|
||||
Binder.get(env)
|
||||
.bind("ca-skeleton.authz", Bindable.of(RolePermissionPolicy.class))
|
||||
.map(RolePermissionPolicy::rolePermissions)
|
||||
.orElse(Map.of());
|
||||
assertThat(bound).containsEntry("site-admin", List.of("studio:read", "studio:write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantsBothReadAndWriteToTheConfiguredRole() {
|
||||
MockEnvironment env = new MockEnvironment();
|
||||
env.setProperty("app.studio.author-role", "site-admin");
|
||||
|
||||
epp.postProcessEnvironment(env, new SpringApplication());
|
||||
|
||||
assertThat(bind(env)).containsEntry("site-admin", List.of("studio:read", "studio:write"));
|
||||
}
|
||||
|
||||
/**
|
||||
* application.yml 이 선언하는 빈 맵을 재현한다. 이것이 매핑을 가리면 Studio 의 모든 쓰기가 403 이 된다 — 읽기는 통과하는데 쓰기만 막히는,
|
||||
* 진단하기 어려운 모양으로 나타난다.
|
||||
*/
|
||||
@Test
|
||||
void survivesAnEmptyMapDeclaredByTheApplicationDefaults() {
|
||||
MockEnvironment env = new MockEnvironment();
|
||||
env.setProperty("app.studio.author-role", "site-admin");
|
||||
env.getPropertySources()
|
||||
.addLast(
|
||||
new MapPropertySource(
|
||||
"applicationDefaults", Map.of("ca-skeleton.authz.role-permissions", "")));
|
||||
|
||||
epp.postProcessEnvironment(env, new SpringApplication());
|
||||
|
||||
assertThat(bind(env)).containsEntry("site-admin", List.of("studio:read", "studio:write"));
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package dev.caskeleton.bootstrap.techlog;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.authz.AuthorizationAdapter;
|
||||
import dev.caskeleton.adapter.inbound.web.authz.RolePermissionPolicy;
|
||||
import dev.caskeleton.adapter.inbound.web.authz.RolePermissionRegistry;
|
||||
import dev.caskeleton.application.security.AuthorizationPort;
|
||||
import dev.caskeleton.shared.security.Permission;
|
||||
import java.util.Set;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
|
||||
|
||||
/**
|
||||
* 후처리기가 심은 매핑이 <em>실행 중인 컨텍스트에서도</em> 살아 있는지 본다.
|
||||
*
|
||||
* <p>바인딩만 따로 확인하면 통과하지만 앱에서는 죽는 경우가 있어서다. {@code MethodSecurityConfig} 의 advisor 는 auto-proxy 보다 먼저
|
||||
* 만들어져야 하는 인프라 빈인데 {@link AuthorizationPort} 를 생성자 파라미터로 받는다. 그래서 {@code AuthorizationAdapter →
|
||||
* RolePermissionRegistry → RolePermissionPolicy} 가 BeanPostProcessor 등록이 끝나기 전에 끌려 올라온다 — 운영 로그가 이
|
||||
* 세 빈에 대해 "not eligible for getting processed by all BeanPostProcessors" 를 정확히 그렇게 찍고 있다.
|
||||
*/
|
||||
class StudioAuthzWiringTest {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(RolePermissionPolicy.class)
|
||||
static class Wiring {
|
||||
|
||||
@Bean
|
||||
RolePermissionRegistry rolePermissionRegistry(RolePermissionPolicy policy) {
|
||||
return new RolePermissionRegistry(policy);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthorizationPort authorizationAdapter(RolePermissionRegistry registry) {
|
||||
return new AuthorizationAdapter(registry);
|
||||
}
|
||||
|
||||
/** MethodSecurityConfig 와 같은 모양: 인프라 advisor 가 포트를 직접 받는다. */
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static Advisor requiresPermissionAuthorizationAdvisor(AuthorizationPort authorizationPort) {
|
||||
AuthorizationManager<MethodInvocation> manager = (authentication, invocation) -> null;
|
||||
return new AuthorizationManagerBeforeMethodInterceptor(
|
||||
AnnotationMatchingPointcut.forMethodAnnotation(Deprecated.class), manager);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void theConfiguredRoleKeepsBothPermissionsInsideARunningContext() {
|
||||
MockEnvironment env = new MockEnvironment();
|
||||
env.setProperty("app.studio.author-role", "site-admin");
|
||||
new StudioAuthzEnvironmentPostProcessor().postProcessEnvironment(env, new SpringApplication());
|
||||
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.setEnvironment(env);
|
||||
context.register(Wiring.class);
|
||||
context.refresh();
|
||||
|
||||
RolePermissionRegistry registry = context.getBean(RolePermissionRegistry.class);
|
||||
assertThat(registry.effectivePermissions(Set.of("site-admin")))
|
||||
.contains(Permission.parse("studio:read"), Permission.parse("studio:write"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user