fix: make Studio authorization actually work, and stop it failing as a 500
Four linked defects, found by driving the 19 operations against a running
backend on PostgreSQL behind a real Keycloak realm.
1. The role→permission mapping never bound. Both profiles wrote it as
role-permissions:
${APP_STUDIO_AUTHOR_ROLE:studio-author}:
- studio:write
and Spring Boot resolves placeholders in @ConfigurationProperties
*values*, not in Map *keys* — the key bound as the literal
"${APP_STUDIO_AUTHOR_ROLE:studio-author}", matched no real role, and
left RolePermissionRegistry empty. Every Studio write answered 403, in
local and prod alike. Setting APP_STUDIO_AUTHOR_ROLE explicitly did not
help; a literal key returned 201 immediately. StudioAuthzEnvironmentPost
Processor now resolves the role name as a scalar (where placeholders do
work) and contributes the mapping under a literal key, so the name stays
deployment-configurable. Registered the same way the tracing bridge is.
2. Reads were unguarded. Only WRITE carried @RequiresPermission, so any
authenticated caller could list every draft and fetch one by id:
listStudioDocuments 200, 2 drafts getStudioDocument 200
getStudioDashboard 200 listStudioAssets 200
The nine read use cases now declare studio:read. They lose `final` for
the same CGLIB reason the write ones already document.
3. Failures were masked. IdempotencyExecutor's catch called store.discard,
whose @Modifying bulk delete needs a transaction and had none, so it
threw TransactionRequiredException over the original exception — the 403
above surfaced as 500 INTERNAL_ERROR with no cause in the log, which is
why this shipped. discard now runs REQUIRES_NEW (cleanup must survive
the failed work's rollback) and a cleanup failure is attached with
addSuppressed instead of replacing what actually went wrong.
4. Reservations leaked. With discard throwing every time, failed requests
left their idempotency rows behind. After the fix only the successful
call's COMPLETED row remains.
Verified end to end: studio-author writes with no extra configuration;
an unprivileged caller gets 403 on all five read operations and on write;
create → save → validate → preview → publish → unpublish all succeed;
optimistic lock returns 409 VERSION_CONFLICT; the publication reaches
public_resource_projection and flips to WITHDRAWN on unpublish.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d5889d644a
commit
37d5614129
+9
@@ -19,6 +19,8 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* DB-backed {@link IdempotencyStorePort}. {@link #tryBegin} uses the {@code uq_idempotency_scope}
|
||||
@@ -162,7 +164,14 @@ public class IdempotencyStoreAdapter implements IdempotencyStorePort {
|
||||
row.getExpiresAt()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code deleteByScope} 는 {@code @Modifying} 벌크 delete 이므로 활성 트랜잭션을 요구한다. 이 메서드는 {@code
|
||||
* IdempotencyExecutor} 의 실패 경로에서 호출되는데 그 지점에는 트랜잭션이 없다 — 예약 레코드를 지우려다 {@code
|
||||
* TransactionRequiredException} 을 던져 원래 실패를 덮고 있었다(403 이 500 으로 바뀌고 로그에 원인이 남지 않았다). REQUIRES_NEW
|
||||
* 인 이유: 정리는 실패한 작업의 롤백에 휩쓸리면 안 된다.
|
||||
*/
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void discard(IdempotencyScope scope) {
|
||||
repository.deleteByScope(
|
||||
IdempotencyRecordEntityMapper.tenantColumn(scope),
|
||||
|
||||
Reference in New Issue
Block a user