The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
15 KiB
Markdown
158 lines
15 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: analysis-finding-a06-f007
|
|
title: 게이트웨이가 적은 순서의 timeout 단계가 정책에 없다
|
|
topic: multitenancy-isolation
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:analysis-finding-a06-f007
|
|
evidenceCapturedOn: 2026-09-04
|
|
body: case-analysis-finding-a06-f007.body.md
|
|
assets:
|
|
- key: analysis-finding-a06-f007
|
|
file: ../../../final/evidence/rendered/analysis-finding-a06-f007.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/analysis-finding-a06-f007.txt
|
|
source:
|
|
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md §25 이다.
|
|
---
|
|
|
|
# 게이트웨이가 적은 순서의 timeout 단계가 정책에 없다
|
|
|
|
`PolicyAwareMongoNativeGateway:11`~`:12` 의 일곱 단계와 `README.md:65`~`:67` 의 열한 단계에 타임아웃이 들어 있다. `MongoNativeOperationPolicy.require` 가 던지는 거부는 여섯이고 타임아웃을 보는 것이 없으며, `ApprovedMongoNativeOperation` 이 선언한 두 예산 값은 어디서도 읽히지 않는다.
|
|
|
|
## 관계
|
|
|
|
- **단일 admission point는 우회 경로를 세어야 성립한다**
|
|
이 게이트웨이는 우회 경로가 아니라 자기가 열거한 단계가 문제다. 정책이 던지는 거부 여섯 가운데 등록 여부와 지원 등급은 두 문서의 목록에 아예 없고, 목록에 있는 타임아웃에는 거부가 없다.
|
|
- **문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다**
|
|
자바독과 README 가 서로 다른 목록을 적는데 둘 다 코드보다 길다.
|
|
- **명령 카탈로그와 admission 아홉 단계**
|
|
Redis 쪽도 자바독이 승인 지점의 검사 순서를 이름으로 열거한 기록이다. 그 개념은 순서의 기준이 비용이라고 적는다.
|
|
|
|
## 문제
|
|
|
|
D3 게이트웨이는 자기 자바독과 모듈 README 양쪽에서 검사 순서를 이름으로 열거한다. 두 목록의 길이가 서로 다르고 둘 다 타임아웃을 담는다.
|
|
|
|
그 단계들이 코드에 있는지 확인했다.
|
|
|
|
## 결론
|
|
|
|
게이트웨이 자신은 세 가지만 한다. :36 이 policy.require(operation) 을 부르고, :39 가 본문을 실행하고, :43 이 감사 기록을 남긴다.
|
|
|
|
검사는 전부 정책에 있고 그 안의 거부는 여섯이다. 등록 여부와 능력 일치와 지원 등급과 두 허용 목록과 관리 범주 차단이며, 각각 :56·:64·:75·:80·:85·:92 에서 던진다.
|
|
|
|
자바독이 적은 일곱 단계 가운데 대응하는 거부가 없는 것은 타임아웃 하나다. ApprovedMongoNativeOperation:26 이 Duration timeout 을 선언하지만, 그 필드가 쓰이는 곳은 :36 의 널 검사와 :40 의 음수 거절과 :58 의 Duration.ZERO 와 :65 의 hasBody() 넷뿐이고 서버로 나가는 자리가 없다.
|
|
|
|
hasBody() 는 정의만 있고 부르는 코드가 없다.
|
|
|
|
:27 의 int maxResults 는 :59 가 0 을 넣는 것 말고 등장하지 않는다. 두 접근자를 부르는 줄이 그 타입을 참조하는 main 파일 넷 전체에서 0 개다.
|
|
|
|
.timeout() 과 .maxResults() 를 실제로 부르는 줄은 같은 모듈에 열여덟 있는데 수신 타입이 전부 다르다. PolicyAwareMongoAggregationExecutor:78 의 effective, DefaultMongoImperativeExecutor:93 의 context, MongoReactiveCursorPublisher:59 의 budget, SpringMongoTransactionSessionFactory:74 의 profile 같은 것들이다.
|
|
|
|
README 쪽 목록에만 있는 다섯 — 연산 이름, 일관성, 결과 제한, 추적, 편집 — 도 마찬가지다.
|
|
|
|
지금은 이 게이트웨이를 지나는 호출이 없다. new PolicyAwareMongoNativeGateway 를 부르는 main 줄이 0 이고 시험은 2 이며, MongoNativeCapabilityGateway 와 PolicyAwareMongoNativeGateway 가 main 에 나오는 셋은 전부 선언이다.
|
|
|
|
그런데도 README 는 이 클래스를 D3 안전성의 근거로 든다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
확인 방식 : 게이트웨이 자바독과 README 의 단계 목록 인용, 게이트웨이 본문 인용, 정책의 require 전문 인용과 거부 자리 계수, 값 타입의 선언과 압축 생성자와 hasBody 인용, 그 타입을 참조하는 main 파일 전수와 그 안의 접근자 호출 계수, 같은 이름 접근자의 다른 타입 호출 전수, 두 게이트웨이 이름의 등장 자리 전수와 생성 계수
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 게이트웨이 자바독과 README 의 두 단계 목록을 나란히 싣는다.
|
|
2. 게이트웨이의 execute 본문을 인용한다.
|
|
3. 정책의 require 를 전문으로 싣고 그 안에서 예외를 던지는 자리를 센다.
|
|
4. 값 타입의 필드 선언과 압축 생성자와 hasBody 를 인용한다.
|
|
5. 그 타입을 참조하는 main 파일을 전부 찾고 그 안에서 .timeout() 과 .maxResults() 를 부르는 줄을 센다.
|
|
6. 같은 이름의 접근자를 부르는 모듈 전체 줄을 나열해 수신 타입을 대조한다.
|
|
7. 두 게이트웨이 이름이 나오는 자리를 소스 세트와 함께 전부 나열하고 생성 줄을 센다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
`PolicyAwareMongoNativeGateway` 는 D3 능력 평면의 게이트웨이다. 클래스 자바독이 검사 순서를 적는다.
|
|
|
|
## 두 문서가 적은 순서
|
|
|
|
:::evidence key="analysis-finding-a06-f007" alt="저장소 루트에서 돌린 정적 검색 출력 180줄. 먼저 PolicyAwareMongoNativeGateway 8~15번 자바독이 실린다. 이것이 D3 능력 게이트웨이이며 설계가 서술한 순서를 실행하고 첫 거부에서 멈춘다고 적는데 그 순서가 등록과 능력과 데이터베이스 프로파일과 컬렉션 프로파일과 타임아웃과 범주 그리고 실행이고, 감사 기록은 연산 아이디와 결과만 담고 BSON 인자는 절대 담지 않으며 데이터를 흘리는 감사 기록은 통제가 아니라 부담이라고 적는다. 그 아래 README 65~67번이 실리는데 D3 는 원시 클라이언트 탈출구가 아니며 이 게이트웨이가 능력에서 데이터베이스 프로파일과 컬렉션 허용 목록과 연산 이름과 타임아웃과 일관성과 결과 제한과 추적과 편집과 명령 범주를 거쳐 D4 차단까지 순서를 고정한다고 적는다. 이어서 게이트웨이 33~46번의 execute 가 실리는데 35번이 널 검사, 36번이 policy.require, 39번이 본문을 데이터베이스 리졸버에 적용해 실행, 43번이 finally 에서 감사 기록을 남기는 것이 전부다. 다음으로 MongoNativeOperationPolicy 52~100번의 require 가 전문으로 실린다. 54~62번이 등록되지 않은 연산을 거절하며 플랫폼은 미리 등록된 구현만 실행하고 호출자가 준 명령은 절대 실행하지 않는다고 적고, 63~72번이 등록된 능력과 제출된 능력이 다르면 거절하며, 73~78번이 지원 등급이 UNSUPPORTED 면 제약과 함께 거절하고, 79~83번이 데이터베이스 프로파일 허용 목록을, 84~90번이 컬렉션 프로파일 허용 목록을 검사하며, 91~97번이 능력 평면에서 허용되지 않는 범주를 거절하면서 drop 과 collMod 와 샤드와 사용자 관리는 자기 자격증명을 가진 D4 관리 평면에서 돈다고 적는다. 그 아래 require 안에서 예외를 던지는 자리가 6 개라고 나온다. 이어서 ApprovedMongoNativeOperation 20~43번이 실리는데 20~28번 record 헤더가 operationId 와 capability 와 category 와 databaseProfile 과 collectionProfile 과 Duration timeout 과 int maxResults 와 body 여덟을 선언하고, 30~43번 압축 생성자가 널 검사 다섯과 빈 아이디 거절과 40~42번의 음수 타임아웃 거절을 한다. 44~61번에는 unregistered 팩터리가 있는데 45~50번 자바독이 이름만 있고 본문이 없는 연산이며 아무도 등록하지 않은 아이디는 실행될 수 없고 그것을 요청하는 모양이 이것이라 게이트웨이의 거부 경로가 도달 가능하고 시험 가능하게 하려고 존재한다고 적고, 58번이 Duration.ZERO 를 59번이 0 을 넣는다. 62~67번의 hasBody 는 timeout 이 양수인지를 돌려준다. 다음으로 ApprovedMongoNativeOperation 을 참조하는 main 파일이 자기 자신과 MongoNativeCapabilityGateway 와 MongoNativeOperationPolicy 와 PolicyAwareMongoNativeGateway 넷이라고 나오고, 그 파일들 안에서 점 timeout 괄호나 점 maxResults 괄호를 부르는 줄이 0 개이며, timeout 이라는 이름이 나오는 줄은 36번의 널 검사와 41번의 음수 거절과 58번의 Duration.ZERO 와 65번의 양수 검사이고, hasBody 를 부르는 줄이 0 개인데 선언까지 포함하면 1 개다. 이어서 대조로 같은 모듈에서 그 두 접근자를 실제로 부르는 열여덟 줄이 나열되는데 PolicyAwareMongoAggregationExecutor 78번과 84번과 92번과 94번, DefaultMongoImperativeExecutor 83번과 93번과 103번, PolicyAwareMongoQueryBuilder 199번, MongoBudgetEnforcer 32번과 40번, DefaultReactiveMongoExecutor 76번과 108번과 154번, MongoReactiveCursorPublisher 59번, MongoResultBudgetTracker 51번과 54번, SpringMongoTransactionSessionFactory 74번, SpringReactiveMongoTransactionSessionFactory 76번이며 수신자가 effective 와 context 와 registered 와 budget 과 requested 와 profile 로 전부 다른 타입이다. 마지막으로 두 게이트웨이 이름이 나오는 자리 전부가 소스 세트와 함께 나오는데 main 은 MongoNativeCapabilityGateway 10번의 인터페이스 선언과 PolicyAwareMongoNativeGateway 16번의 클래스 선언과 24번의 생성자 셋뿐이고 나머지 여섯은 PolicyAwareMongoNativeGatewayTest 의 것이며, new PolicyAwareMongoNativeGateway 를 부르는 main 줄이 0 개이고 시험에서는 2 개다." caption="게이트웨이 자바독의 일곱 단계와 README 의 열한 단계 · execute 가 하는 세 가지 · require 전문과 거부 여섯 · 값 타입이 선언한 timeout 과 maxResults 와 hasBody · 그 타입을 참조하는 넷 안에서 접근자 호출 0 · 같은 접근자를 실제로 적용하는 다른 타입 열여덟 줄 · main 참조 셋과 생성 0 — 180줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
`:11`\~`:12` 가 일곱을 적는다. 등록, 능력, 데이터베이스 프로파일, 컬렉션 프로파일, 타임아웃, 범주, 그리고 실행이다. 설계가 서술한 순서를 실행하고 첫 거부에서 멈춘다고 적는다.
|
|
|
|
`README.md:65`\~`:67` 은 더 길다. 능력에서 시작해 데이터베이스 프로파일과 컬렉션 허용 목록과 연산 이름과 타임아웃과 일관성과 결과 제한과 추적과 편집과 명령 범주를 거쳐 D4 차단까지 열한 단계다.
|
|
|
|
## execute 안의 세 줄
|
|
|
|
`:34` 의 `execute` 안에서 `:36` 이 `policy.require(operation)` 을 부른다.
|
|
|
|
`:39` 가 `operation.body()` 를 데이터베이스 리졸버가 준 `MongoDatabase` 에 적용한다.
|
|
|
|
`:43` 이 `finally` 에서 연산 아이디와 성공 여부를 감사 기록기에 넘긴다.
|
|
|
|
검사는 전부 정책에 있다.
|
|
|
|
## 정책이 던지는 거부는 여섯이다
|
|
|
|
`MongoNativeOperationPolicy.require:52`\~`:100` 안에서 `MongoOperationRejectedException` 을 던지는 자리가 여섯이다.
|
|
|
|
`:56` 이 등록되지 않은 연산을 거절한다. `:58`\~`:61` 의 메시지가 플랫폼은 미리 등록된 구현만 실행하고 호출자가 준 명령은 절대 실행하지 않는다고 적는다.
|
|
|
|
`:64` 가 등록된 능력과 제출된 능력의 불일치를, `:75` 가 `UNSUPPORTED` 등급을, `:80` 이 데이터베이스 프로파일 허용 목록을, `:85` 가 컬렉션 프로파일 허용 목록을 본다.
|
|
|
|
`:92` 가 능력 평면에서 허용되지 않는 범주를 거절하면서, `drop` 과 `collMod` 와 샤드와 사용자 관리는 자기 자격증명을 가진 D4 관리 평면에서 돈다고 적는다.
|
|
|
|
타임아웃을 보는 자리가 없다.
|
|
|
|
## 선언은 돼 있고 읽히지 않는다
|
|
|
|
`ApprovedMongoNativeOperation:26` 이 `Duration timeout` 을, `:27` 이 `int maxResults` 를 선언한다.
|
|
|
|
`timeout` 이 쓰이는 자리는 넷이다. `:36` 이 널 검사를 하고, `:40`\~`:42` 가 음수를 거절하고, `:58` 이 `unregistered` 팩터리에서 `Duration.ZERO` 를 넣고, `:65` 의 `hasBody()` 가 양수인지를 돌려준다. 값을 검사하거나 채울 뿐 서버에 보내는 자리가 없다.
|
|
|
|
`:45`\~`:50` 자바독이 그 팩터리의 목적을 적는다. 아무도 등록하지 않은 아이디는 실행될 수 없고 그것을 요청하는 모양이 이것이라, 게이트웨이의 거부 경로가 도달 가능하고 시험 가능하게 하려는 것이다. `:59` 가 `maxResults` 자리에 `0` 을 넣는다.
|
|
|
|
`hasBody()` 를 부르는 줄은 0 개다. 선언까지 포함해도 1 이므로, 그 이름이 나오는 자리는 정의 한 줄뿐이다.
|
|
|
|
`maxResults` 는 `:27` 의 선언 이후 한 번도 등장하지 않는다. `ApprovedMongoNativeOperation` 을 참조하는 main 파일 넷 — 자기 자신과 `MongoNativeCapabilityGateway` 와 `MongoNativeOperationPolicy` 와 `PolicyAwareMongoNativeGateway` — 안에서 `.timeout()` 이나 `.maxResults()` 를 부르는 줄이 0 개다.
|
|
|
|
## .timeout() 과 .maxResults() 를 부르는 열여덟 줄
|
|
|
|
같은 모듈에서 그 두 접근자를 부르는 줄은 열여덟이다. 수신자가 전부 다른 타입이다.
|
|
|
|
`PolicyAwareMongoAggregationExecutor:78` 은 `effective.maxResults()` 로 결과 수를 견주고 `:92` 는 `context.timeout()` 을 밀리초로 바꾼다. `DefaultMongoImperativeExecutor:93` 은 경과 시간을 `context.timeout()` 과 견준다. `MongoReactiveCursorPublisher:59` 는 `budget.maxResults()` 로 커서를 제한하고, `MongoResultBudgetTracker:51` 은 그 수를 넘으면 실패시킨다. `SpringMongoTransactionSessionFactory:74` 는 `profile.timeout()` 을 트랜잭션 옵션에 넣는다.
|
|
|
|
같은 모듈의 다른 타입들은 두 접근자를 열여덟 줄에서 실제로 적용한다. `ApprovedMongoNativeOperation` 이 선언한 두 값만 어디서도 읽히지 않는다.
|
|
|
|
README 가 적은 일관성과 추적과 편집과 연산 이름 단계는 정책에 대응하는 거부가 아예 없다.
|
|
|
|
## 지금 조립되지 않는다
|
|
|
|
두 이름이 main 에 나오는 줄은 셋이다. `MongoNativeCapabilityGateway:10` 의 인터페이스 선언과 `PolicyAwareMongoNativeGateway:16` 의 클래스 선언과 `:24` 의 생성자다.
|
|
|
|
`new PolicyAwareMongoNativeGateway` 를 부르는 main 줄이 0 이다. 나머지 여섯 줄은 `PolicyAwareMongoNativeGatewayTest` 의 것이다.
|
|
|
|
그래도 `README.md:65` 는 D3 가 원시 클라이언트 탈출구가 아닌 근거로 이 클래스를 든다. 그 클래스를 만드는 main 줄은 0 이고, 자바독이 적은 일곱 단계 중 타임아웃에 대응하는 거부가 없으며, README 에만 있는 다섯 단계에도 없다.
|
|
|
|
## 원문에 없는 것
|
|
|
|
원문은 문서가 열거한 단계 중 여럿이 코드에 없고 그중 둘은 값 타입이 필드로 선언까지 해 둔 것이라고 적는다.
|
|
|
|
여기에 더한 것은 그 판정을 검색 실패와 가르는 대조다. `.timeout()` 과 `.maxResults()` 는 같은 모듈에서 열여덟 번 불리는데 전부 다른 타입이고, D3 값 타입을 참조하는 파일 넷 안에서만 0 이다. `hasBody()` 도 저장소 전체에서 호출자가 없다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
배선된 뒤에 어느 단계가 꼭 있어야 하는지는 설계 문서를 따로 읽어 판단하지 않았다.
|
|
|
|
선언된 타임아웃을 실제로 걸려면 드라이버의 어느 옵션에 실어야 하는지 조사하지 않았다.
|
|
|
|
추적과 편집이 다른 계층에서 이미 이뤄지는지는 이 모듈 밖이라 보지 않았다.
|
|
|
|
<!-- body:end -->
|