`ObjectStorageAssetBinaryAdapter` bridges Studio's asset port to the object
storage port through an `ObjectProvider`, which means it compiles whether or
not an implementation is on the classpath. None was: app-bootstrap never
depended on `:adapter:outbound:objectstorage`, so the provider was always
empty and every upload and delete answered STUDIO_UNAVAILABLE with the message
"set ca-skeleton.objectstorage.* to enable" — configuration advice for a
missing dependency, which sends the reader looking in the wrong place.
The AWS SDK BOM has to be imported here as well. The objectstorage module
imports it at module scope on purpose (its comment explains: keep the strict
locking blast radius contained), and Spring's dependency management does not
propagate to consumers, so assembling the runtime here left s3 and
netty-nio-client without versions. The grpc module has the same shape and did
not surface it because app-bootstrap only consumes grpc from a test
configuration; this is the first runtime consumer of that pattern.
Lock state regenerated for the SDK's transitive set.
ActuatorSecurityHttpTest.healthEndpointIsPermitAll fails on this branch before
this change as well; it is untouched here.
auth-mode=redis-session was unreachable: getStudioSession answered 503 on
every call because the CSRF token is null when CsrfFilter never runs, and
CsrfFilter only runs in the session branch, which could not be selected
because AuthenticationModeCompositionConfig requires a
`redisVersionedSessionRepository` / `springSessionRepositoryFilter` pair
and only the second existed. Even with the pair present nothing could
create a session — there was no login endpoint and no client registration.
This is the surface the contract already describes: securitySchemes
declares a session cookie plus X-CSRF-TOKEN on mutations, not a bearer
token, and SecurityConfig's session branch (cookie CSRF repository,
session-fixation migration) plus RedisSessionWebConfig (servlet filter,
host-only cookie) were already written for it. The SPA never holds a
token; the backend owns the session.
- StudioSessionInfrastructureConfig supplies the missing repository
under the name the composition validator looks for. @EnableRedisHttp
Session is not used because it pins the bean name to sessionRepository.
- StudioOidcLoginSuccessHandler converts the OidcUser into an
AuthenticatedPrincipal. PrimitiveSessionSecurityContextRepository
rejects anything else on save — deliberately, so credentials and
framework object graphs never cross the session boundary — and it
restores the same type on load. Roles are unioned from realm_access
and resource_access exactly as the JWT converter does, so both modes
resolve the same set and the studio:read / studio:write mapping
behaves identically.
- SecurityConfig wires oauth2Login (only when a success handler bean is
present, so JWT mode is untouched) and a /logout that invalidates the
session. The envelope 401 stays the entry point: an unauthenticated
API call must not answer 302, which an XHR cannot follow. The SPA
navigates the browser to /oauth2/authorization/{id} instead.
Verified in a browser against a real Keycloak realm:
/oauth2/authorization/keycloak → Keycloak → callback
TECHLOG_SESSION cookie set, httpOnly
GET /api/v1/studio/session 200 {authenticated, displayName, roles,
csrfToken, csrfHeaderName}
POST /api/v1/studio/documents 403 without the CSRF header
201 with it
GET /api/v1/studio/documents 200
Also removes the same broken placeholder-in-map-key role mapping from the
dev profile that the previous commit fixed in local and prod.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
설계 패키지의 studio-v1.yaml(v3.0.0, 응답 봉투)을 이 저장소에 배선하고
슬라이스 1의 기반을 세운다. 19개 operation 중 getStudioSession과
listStudioCatalog를 구현했다.
계약과 생성
- src/config/openapi/studio-v1.yaml 을 vendor하고 MANIFEST에 출처 커밋을 기록
- openapi-generator로 DTO(model)만 생성한다. generateApis 대신
globalProperties.set(['models': '']) — 그 두 속성은 플러그인 7.18.0에 없다
- useOneOfInterfaces=false. 그 대가로 discriminator union 5종의 Jackson 배선이
깨진다(spec §3.1). 그 5종을 쓰는 7개 operation은 Plan 02에서 전략을 정한 뒤 구현한다
- 생성 코드는 별도 generatedOpenapi sourceSet에 둔다. -Werror가 생성물의 deprecated
API 사용을 빌드 실패로 승격하기 때문이다. jar와 test 클래스패스에 별도로 얹는다
오류 계약
- StudioError 23종(계약 ApiError.code와 1:1) + StudioException(ApiErrorCarrier)
- StudioExceptionHandler는 techlog 패키지로 범위를 좁힌다. 다른 기능의 오류 응답을
바꾸지 않기 위해서다
- 클라이언트 문구는 레지스트리의 client_safe_message에서 가져오고 예외 메시지는
로그 전용이다(ApiErrorCarrier javadoc의 요구)
- 바인딩 예외를 봉투로 옮긴다. 그러지 않으면 bare RFC 7807이 새어 나가 ADR-006을 위반한다
게이트
- TechLogBoundaryArchTest 7종 — spec §4.3의 bounded context 경계. Gradle leaf를
늘릴 수 없어 이 규칙이 경계의 유일한 방어선이다
- StudioErrorRegistryTest — enum ↔ 레지스트리 ↔ 계약 3축 대조, vendor 사본 해시 검증
- StudioContractDriftTest — springdoc 표면이 계약을 벗어나면 실패. @ComponentScan이라
새 컨트롤러가 자동으로 걸린다
- StudioSessionCsrfHeaderProfileContractTest — 배포 가능한 세 프로파일이 계약의
csrf-header-name const로 해소되는지 고정. 이 저장소는 실제 composition root를
테스트에서 부팅할 수 없어 파일 단언으로 그 층을 덮는다
스키마
- V7__techlog_core.sql, 28 테이블. 설계 DDL에서 studio_idempotency(기존
idempotency_record 재사용)와 범위 밖 6종을 제외했다
- 원본의 tech_log 스키마 대신 public을 쓴다. 원본의 SET search_path는 Flyway
세션에만 적용되고 런타임 커넥션 풀은 상속하지 않는다
알려진 제약
- getStudioSession은 세션 인프라(redis-session)가 없어 503 STUDIO_UNAVAILABLE을
반환한다. 계약이 이 operation에 허용하는 유일한 실패 코드다. 가짜 CSRF 토큰으로
200을 만들지 않았다
- 따라서 슬라이스 1의 "프론트 로그인 실동작" 목표는 아직 달성되지 않았다
이 커밋은 AGENTS.md의 human-only 커밋 정책에 대한 저장소 소유자의 명시적 지시로
작성됐다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>