refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,23 +1,23 @@
{
"version": "1.1",
"id": "redis-admission-stages",
"title": "명령 입장의 단일 지점",
"question": "명령은 어디서 걸러지는가?",
"type": "dependency",
"title": "의도된 guarded path와 실제 direct gateway 우회",
"question": "Redis 명령은 guard를 통과하는가, semantic adapter에서 gateway로 우회하는가?",
"type": "flow",
"direction": "LR",
"audience": [
"이 저장소의 구조를 읽는 사람"
"백엔드 엔지니어"
],
"summary": "카탈로그를 통과하면 실행이고 미분류나 BLOCKED 이면 fail-closed 로 거절된다.",
"alt": "CommandPolicyGuard 에서 카탈로그 통과는 실행으로 미분류와 BLOCKED 는 거절로 갈린다.",
"long_description": "SSOT 는 이 구조의 바닥이 카탈로그가 미분류 명령을 fail-closed 로 거부하는 것이라고 적는다.",
"summary": "guarded command path에서는 CommandPolicyGuard가 admission을 담당하지만, 현재 semantic adapter 다섯은 gateway를 직접 호출해 이 경로를 우회한다.",
"alt": "의도된 command path는 CommandPolicyGuard를 거쳐 실행 또는 fail-closed 거절로 갈리고, 별도의 semantic adapter 경로는 RedisCommandGateway를 직접 호출해 guard를 우회하는 흐름도",
"long_description": "CommandPolicyGuard는 guarded command path의 admission 지점이다. 그러나 현재 semantic adapter 다섯은 RedisLease에서 gateway를 직접 얻어 호출하므로 catalog, permit, slot, budget, translation, observation 단계가 이 경로에 적용되지 않는다.",
"source_context": {
"document": "/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/clean-architecture-backend-template/final/document.md",
"document_sha256": "8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d",
"document": "docs/clean-architecture-backend-template/final/document.md",
"document_sha256": "7c986b30b6ef3c12060b6749ee60d53e37d6994493d2703419732c9cab6077d8",
"anchor": {
"kind": "line",
"value": 13463,
"line": 13463
"value": 13469,
"line": 13469
}
},
"composition": {
@@ -26,20 +26,33 @@
"reference_ids": [
"payment-event-flow"
],
"rationale": "한 지점이 모든 명령을 두 결과로 가른다는 것이 논지다.",
"focus_node": "guard"
"rationale": "의도된 guarded path와 실제 bypass path를 한 화면에서 대비해야 전체 runtime의 single admission으로 오해하지 않는다.",
"focus_node": "bypass"
},
"groups": [],
"nodes": [
{
"id": "guard",
"label": "CommandPolicyGuard",
"id": "typed-entry",
"label": "guarded command path",
"kind": "service",
"role": "source",
"evidence": [
{
"start_line": 13463,
"end_line": 13490
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false
},
{
"id": "guard",
"label": "CommandPolicyGuard",
"kind": "service",
"role": "service",
"evidence": [
{
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false,
@@ -47,69 +60,150 @@
},
{
"id": "run",
"label": "실행",
"kind": "service",
"label": "승인 후 실행",
"kind": "result",
"role": "sink",
"evidence": [
{
"start_line": 13463,
"end_line": 13490
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false,
"details": [
"카탈로그 통과"
]
"assumption": false
},
{
"id": "deny",
"label": "fail-closed 거절",
"kind": "result",
"role": "sink",
"evidence": [
{
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false
},
{
"id": "semantic",
"label": "semantic adapters ×5",
"kind": "service",
"role": "source",
"evidence": [
{
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false
},
{
"id": "gateway",
"label": "RedisCommandGateway 직접 호출",
"kind": "service",
"role": "sink",
"evidence": [
{
"start_line": 13463,
"end_line": 13497
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false
},
{
"id": "bypass",
"label": "guard stages 우회",
"kind": "result",
"role": "sink",
"evidence": [
{
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false,
"emphasis": "primary",
"details": [
"미분류 · BLOCKED"
"catalog · permit · slot · budget",
"translation · observation"
]
}
],
"edges": [
{
"id": "a",
"id": "entry",
"from": "typed-entry",
"to": "guard",
"label": "admission",
"kind": "request",
"evidence": [
{
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false
},
{
"id": "pass",
"from": "guard",
"to": "run",
"label": "통과",
"kind": "request",
"kind": "data",
"evidence": [
{
"start_line": 13463,
"end_line": 13490
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false,
"emphasis": "primary"
"assumption": false
},
{
"id": "b",
"id": "reject",
"from": "guard",
"to": "deny",
"label": "거절",
"kind": "data",
"evidence": [
{
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false
},
{
"id": "direct",
"from": "semantic",
"to": "gateway",
"label": "lease.gateway()",
"kind": "request",
"evidence": [
{
"start_line": 13463,
"end_line": 13497
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false,
"style": "dashed"
},
{
"id": "skips",
"from": "gateway",
"to": "bypass",
"label": "guard 미경유",
"kind": "data",
"evidence": [
{
"start_line": 13469,
"end_line": 13503
}
],
"assumption": false,
"style": "dashed",
"emphasis": "primary"
}
],
"legend": [],
"metadata": {}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,140 +1,146 @@
{
"version": "1.1",
"id": "rls-three-preconditions",
"title": "격리가 성립하는 세 조건",
"question": "RLS 격리는 무엇이 동시에 참이어야 성립하는가?",
"type": "sequence",
"direction": "LR",
"title": "PostgreSQL RLS 적용 여부를 가르는 분기",
"question": "현재 role과 table에서 RLS policy가 실제로 적용되는가?",
"type": "data-flow",
"direction": "TB",
"audience": [
"이 저장소의 구조를 읽는 사람"
"백엔드 엔지니어"
],
"summary": "ENABLE RLS 와 FORCE RLS 와 BYPASSRLS 없는 런타임 롤 셋이 동시에 참이어야 격리가 성립한다.",
"alt": "격리 판정이 ENABLE RLS 와 FORCE RLS 와 BYPASSRLS 없는 롤을 차례로 확인하는 순서.",
"long_description": "SSOT 는 RlsPolicyVerifier.requireEnforced 가 런타임 롤의 BYPASSRLS 를 확인하고 current_schema() 의 실제 테이블을 순회하며 tenant-scoped 목록에 든 것만 검사한다고 적는다.",
"summary": "RLS 활성 여부, 우회 role, owner와 FORCE RLS, applicable policy 유무를 차례로 구분한다.",
"alt": "RLS 비활성은 policy 미적용으로, superuser와 BYPASSRLS는 우회로, owner는 FORCE 여부로 갈리고, policy 대상인데 applicable policy가 없으면 default deny가 되는 흐름도",
"long_description": "PostgreSQL RLS를 세 개의 동시 전제로 보지 않는다. RLS가 활성화된 뒤 superuser 또는 BYPASSRLS인지, table owner인지와 FORCE RLS 여부를 확인한다. policy 대상 role에 applicable policy가 없으면 default deny이고, policy가 있으면 USING과 WITH CHECK를 평가한다.",
"source_context": {
"document": "/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/clean-architecture-backend-template/final/document.md",
"document_sha256": "8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d",
"document": "docs/clean-architecture-backend-template/final/document.md",
"document_sha256": "7c986b30b6ef3c12060b6749ee60d53e37d6994493d2703419732c9cab6077d8",
"anchor": {
"kind": "line",
"value": 7398,
"line": 7398
"value": 7400,
"line": 7400
}
},
"composition": {
"profile": "sequence",
"profile": "two-zone-pipeline",
"diagram_only": true,
"reference_ids": [
"payment-approval-sequence"
"localization-pipeline"
],
"rationale": "세 조건이 차례로 확인되어야 격리가 성립한다는 순서가 논지다.",
"focus_node": "verifier"
"rationale": "정책 적용 여부를 결정하는 검사 경로와 각 단계에서 빠져나가는 결과를 짧은 파이프라인으로 보여 준다.",
"focus_node": "policy"
},
"groups": [],
"nodes": [
"groups": [
{
"id": "start",
"label": "격리 판정",
"kind": "service",
"role": "participant",
"id": "role",
"label": "policy 적용 대상 판정",
"evidence": [
{
"start_line": 7396,
"end_line": 7420
"start_line": 7400,
"end_line": 7432
}
],
"assumption": false
},
{
"id": "verifier",
"label": "RlsPolicyVerifier",
"kind": "service",
"role": "participant",
"id": "policy-zone",
"label": "policy 존재와 평가",
"evidence": [
{
"start_line": 7396,
"end_line": 7420
"start_line": 7400,
"end_line": 7432
}
],
"assumption": false
}
],
"nodes": [
{
"id": "rls",
"label": "RLS 활성 여부",
"kind": "process",
"role": "stage",
"evidence": [
{
"start_line": 7400,
"end_line": 7432
}
],
"assumption": false,
"emphasis": "primary"
"group": "role",
"details": [
"no → policy 미적용"
]
},
{
"id": "db",
"label": "데이터베이스",
"kind": "database",
"role": "participant",
"id": "subject",
"label": "policy 적용 대상",
"kind": "process",
"role": "stage",
"evidence": [
{
"start_line": 7396,
"end_line": 7426
"start_line": 7400,
"end_line": 7432
}
],
"assumption": false
"assumption": false,
"group": "role",
"details": [
"superuser / BYPASSRLS → 우회",
"owner + FORCE off → 우회",
"non-owner 또는 owner + FORCE on → 대상"
]
},
{
"id": "policy",
"label": "applicable policy",
"kind": "process",
"role": "stage",
"evidence": [
{
"start_line": 7400,
"end_line": 7432
}
],
"assumption": false,
"group": "policy-zone",
"emphasis": "primary",
"details": [
"none → default deny",
"exists → USING / WITH CHECK 평가"
]
}
],
"edges": [
{
"id": "m1",
"from": "start",
"to": "verifier",
"label": "검증 요청",
"id": "e1",
"from": "rls",
"to": "subject",
"label": "RLS on",
"kind": "request",
"evidence": [
{
"start_line": 7396,
"end_line": 7420
"start_line": 7400,
"end_line": 7432
}
],
"assumption": false,
"order": 1
"assumption": false
},
{
"id": "m2",
"from": "verifier",
"to": "db",
"label": "ENABLE RLS 확인",
"id": "e2",
"from": "subject",
"to": "policy",
"label": "적용",
"kind": "request",
"evidence": [
{
"start_line": 7396,
"end_line": 7420
"start_line": 7400,
"end_line": 7432
}
],
"assumption": false,
"order": 2
},
{
"id": "m3",
"from": "verifier",
"to": "db",
"label": "FORCE RLS 확인",
"kind": "request",
"evidence": [
{
"start_line": 7396,
"end_line": 7420
}
],
"assumption": false,
"order": 3
},
{
"id": "m4",
"from": "verifier",
"to": "db",
"label": "BYPASSRLS 없음 확인",
"kind": "request",
"evidence": [
{
"start_line": 7396,
"end_line": 7426
}
],
"assumption": false,
"order": 4,
"emphasis": "primary"
}
],
"legend": [],
"metadata": {}
}
}
@@ -1,20 +1,27 @@
# 명령 입장의 단일 지점
# 의도된 guarded path와 실제 direct gateway 우회
## Alternative text
CommandPolicyGuard 에서 카탈로그 통과는 실행으로 미분류와 BLOCKED 는 거절로 갈린다.
의도된 command path는 CommandPolicyGuard를 거쳐 실행 또는 fail-closed 거절로 갈리고, 별도의 semantic adapter 경로는 RedisCommandGateway를 직접 호출해 guard를 우회하는 흐름도
## Long description
SSOT 는 이 구조의 바닥이 카탈로그가 미분류 명령을 fail-closed 로 거부하는 것이라고 적는다.
CommandPolicyGuard는 guarded command path의 admission 지점이다. 그러나 현재 semantic adapter 다섯은 RedisLease에서 gateway를 직접 얻어 호출하므로 catalog, permit, slot, budget, translation, observation 단계가 이 경로에 적용되지 않는다.
## Elements and evidence
- **CommandPolicyGuard** (service): No additional description. Evidence: L13463L13490.
- **실행** (service): No additional description. Evidence: L13463L13490.
- **fail-closed 거절** (service): No additional description. Evidence: L13463L13497.
- **guarded command path** (service): No additional description. Evidence: L13469L13503.
- **CommandPolicyGuard** (service): No additional description. Evidence: L13469L13503.
- **승인 후 실행** (result): No additional description. Evidence: L13469L13503.
- **fail-closed 거절** (result): No additional description. Evidence: L13469L13503.
- **semantic adapters ×5** (service): No additional description. Evidence: L13469L13503.
- **RedisCommandGateway 직접 호출** (service): No additional description. Evidence: L13469L13503.
- **guard stages 우회** (result): No additional description. Evidence: L13469L13503.
## Relationships
- **CommandPolicyGuard → 실행:** 통과. Evidence: L13463L13490.
- **CommandPolicyGuard → fail-closed 거절:** 거절. Evidence: L13463L13497.
- **semantic adapters ×5 → RedisCommandGateway 직접 호출:** lease.gateway(). Evidence: L13469L13503.
- **guarded command path → CommandPolicyGuard:** admission. Evidence: L13469L13503.
- **CommandPolicyGuard → 승인 후 실행:** 통과. Evidence: L13469L13503.
- **CommandPolicyGuard → fail-closed 거절:** 거절. Evidence: L13469L13503.
- **RedisCommandGateway 직접 호출 → guard stages 우회:** guard 미경유. Evidence: L13469L13503.
@@ -1,14 +1,29 @@
# 명령 입장의 단일 지점
# Question: 명령은 어디서 걸러지는가?
# 의도된 guarded path와 실제 direct gateway 우회
# Question: Redis 명령은 guard를 통과하는가, semantic adapter에서 gateway로 우회하는가?
direction: right
n0: "CommandPolicyGuard" {
n0: "guarded command path" {
shape: rectangle
}
n1: "실행" {
n1: "CommandPolicyGuard" {
shape: rectangle
}
n2: "fail-closed 거절" {
n2: "승인 후 실행" {
shape: rectangle
}
n0 -> n1: "통과"
n0 -> n2: "거절"
n3: "fail-closed 거절" {
shape: rectangle
}
n4: "semantic adapters ×5" {
shape: rectangle
}
n5: "RedisCommandGateway 직접 호출" {
shape: rectangle
}
n6: "guard stages 우회" {
shape: rectangle
}
n0 -> n1: "admission"
n1 -> n2: "통과"
n1 -> n3: "거절"
n4 -> n5: "lease.gateway()"
n5 -> n6: "guard 미경유"
@@ -0,0 +1,17 @@
digraph techviz {
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="guarded command path", shape=box, style="rounded,filled"];
n1 [label="CommandPolicyGuard", shape=box, style="rounded,filled"];
n2 [label="승인 후 실행", shape=box, style="rounded,filled"];
n3 [label="fail-closed 거절", shape=box, style="rounded,filled"];
n4 [label="semantic adapters ×5", shape=box, style="rounded,filled"];
n5 [label="RedisCommandGateway 직접 호출", shape=box, style="rounded,filled"];
n6 [label="guard stages 우회", shape=box, style="rounded,filled"];
n0 -> n1 [label="admission", style=solid];
n1 -> n2 [label="통과", style=solid];
n1 -> n3 [label="거절", style=solid];
n4 -> n5 [label="lease.gateway()", style=solid];
n5 -> n6 [label="guard 미경유", style=solid];
}
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="redis-admission-stages" name="의도된 guarded path와 실제 direct gateway 우회">
<mxGraphModel dx="1055" dy="465" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1055" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_typed-entry" value="guarded command path" tooltip="service | Evidence: L13469-L13503" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="70.0" y="140.0" width="174.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_semantic" value="semantic adapters ×5" tooltip="service | Evidence: L13469-L13503" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="70.0" y="276.0" width="174.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_guard" value="CommandPolicyGuard" tooltip="service | Evidence: L13469-L13503" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="418.0" y="135.0" width="160.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_gateway" value="RedisCommandGateway 직접 호출" tooltip="service | Evidence: L13469-L13503" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="404.0" y="271.0" width="188.0" height="74.0" as="geometry"/>
</mxCell>
<mxCell id="n_deny" value="fail-closed 거절" tooltip="result | Evidence: L13469-L13503" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="806.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_run" value="승인 후 실행" tooltip="result | Evidence: L13469-L13503" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="806.0" y="196.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_bypass" value="guard stages 우회&lt;br/&gt;catalog · permit · slot · budget&lt;br/&gt;translation · observation" tooltip="result | Evidence: L13469-L13503" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="752.0" y="332.0" width="258.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="e_direct" value="lease.gateway()" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_semantic" target="n_gateway">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.0" y="280.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_entry" value="admission" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_typed-entry" target="n_guard">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355.0" y="169.5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_pass" value="통과" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_guard" target="n_run">
<mxGeometry relative="1" as="geometry">
<mxPoint x="716.0" y="202.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_reject" value="거절" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_guard" target="n_deny">
<mxGeometry relative="1" as="geometry">
<mxPoint x="716.0" y="125.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_skips" value="guard 미경유" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_gateway" target="n_bypass">
<mxGeometry relative="1" as="geometry">
<mxPoint x="696.0" y="342.0" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,991 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "edge-direct",
"type": "arrow",
"x": 244.0,
"y": 308.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 893186682,
"version": 1,
"versionNonce": 455485550,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-semantic",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-gateway",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-direct",
"type": "text",
"x": 264.0,
"y": 268.0,
"width": 120,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 687374111,
"version": 1,
"versionNonce": 1753930037,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "lease.gateway()",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "lease.gateway()",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-entry",
"type": "arrow",
"x": 244.0,
"y": 167.0,
"width": 174.0,
"height": 5.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1091082879,
"version": 1,
"versionNonce": 1327755587,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
5.0
],
[
87.0,
5.0
],
[
87.0,
0.0
],
[
174.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-typed-entry",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-guard",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-entry",
"type": "text",
"x": 310.0,
"y": 157.5,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1236462406,
"version": 1,
"versionNonce": 215795817,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "admission",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "admission",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-pass",
"type": "arrow",
"x": 578.0,
"y": 176.0,
"width": 228.0,
"height": 52.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 610606581,
"version": 1,
"versionNonce": 1519236829,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
114.0,
0.0
],
[
114.0,
52.0
],
[
228.0,
52.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-guard",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-run",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-pass",
"type": "text",
"x": 671.0,
"y": 190.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 783585354,
"version": 1,
"versionNonce": 46708051,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "통과",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "통과",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-reject",
"type": "arrow",
"x": 578.0,
"y": 92.0,
"width": 228.0,
"height": 66.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1747658786,
"version": 1,
"versionNonce": 322244831,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
66.0
],
[
114.0,
66.0
],
[
114.0,
0.0
],
[
228.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-guard",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-deny",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-reject",
"type": "text",
"x": 671.0,
"y": 113.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1904447632,
"version": 1,
"versionNonce": 1512441695,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "거절",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "거절",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-skips",
"type": "arrow",
"x": 592.0,
"y": 308.0,
"width": 160.0,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1567695484,
"version": 1,
"versionNonce": 138426472,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
68.0
],
[
160.0,
68.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-gateway",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-bypass",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-skips",
"type": "text",
"x": 651.0,
"y": 330.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 527325182,
"version": 1,
"versionNonce": 1660368872,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "guard 미경유",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "guard 미경유",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-typed-entry",
"type": "rectangle",
"x": 70.0,
"y": 140.0,
"width": 174.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1422067438,
"version": 1,
"versionNonce": 184283477,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-typed-entry",
"type": "text",
"x": 80.0,
"y": 150.0,
"width": 154.0,
"height": 44.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1597371070,
"version": 1,
"versionNonce": 116582098,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "guarded command path",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "guarded command path",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-semantic",
"type": "rectangle",
"x": 70.0,
"y": 276.0,
"width": 174.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1337464297,
"version": 1,
"versionNonce": 101804627,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-semantic",
"type": "text",
"x": 80.0,
"y": 286.0,
"width": 154.0,
"height": 44.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1868596939,
"version": 1,
"versionNonce": 1104547463,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "semantic adapters ×5",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "semantic adapters ×5",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-guard",
"type": "rectangle",
"x": 418.0,
"y": 135.0,
"width": 160.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 287186016,
"version": 1,
"versionNonce": 258088240,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-guard",
"type": "text",
"x": 428.0,
"y": 145.0,
"width": 140.0,
"height": 44.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1734755542,
"version": 1,
"versionNonce": 1622291566,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "CommandPolicyGuard",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "CommandPolicyGuard",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-gateway",
"type": "rectangle",
"x": 404.0,
"y": 271.0,
"width": 188.0,
"height": 74.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1264474515,
"version": 1,
"versionNonce": 568841533,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-gateway",
"type": "text",
"x": 414.0,
"y": 281.0,
"width": 168.0,
"height": 54.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 222918616,
"version": 1,
"versionNonce": 1589548629,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "RedisCommandGateway 직접 호출",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "RedisCommandGateway 직접 호출",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-deny",
"type": "rectangle",
"x": 806.0,
"y": 60.0,
"width": 150.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 763196993,
"version": 1,
"versionNonce": 1443081913,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-deny",
"type": "text",
"x": 816.0,
"y": 70.0,
"width": 130.0,
"height": 44.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 804770357,
"version": 1,
"versionNonce": 725474116,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "fail-closed 거절",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "fail-closed 거절",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-run",
"type": "rectangle",
"x": 806.0,
"y": 196.0,
"width": 150.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 993733559,
"version": 1,
"versionNonce": 1523859840,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-run",
"type": "text",
"x": 816.0,
"y": 206.0,
"width": 130.0,
"height": 44.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1656583306,
"version": 1,
"versionNonce": 187733575,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "승인 후 실행",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "승인 후 실행",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-bypass",
"type": "rectangle",
"x": 752.0,
"y": 332.0,
"width": 258.0,
"height": 88.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 994055316,
"version": 1,
"versionNonce": 1666152370,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-bypass",
"type": "text",
"x": 762.0,
"y": 342.0,
"width": 238.0,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1029604402,
"version": 1,
"versionNonce": 219747915,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "guard stages 우회\ncatalog · permit · slot · budget\ntranslation · observation",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "guard stages 우회\ncatalog · permit · slot · budget\ntranslation · observation",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -2,19 +2,23 @@
"harness_version": "0.2.0",
"spec_id": "redis-admission-stages",
"spec_version": "1.1",
"spec_sha256": "fe6a87016b5442d9611f3a88010b837794bd92cbcb3f65504f5215dcccc4e187",
"spec_sha256": "1f1dfcffe5be16d6b9303aab638c22acc1ae19d30bd309504f047dc9e673732b",
"source_context": {
"document": "/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/clean-architecture-backend-template/final/document.md",
"document_sha256": "8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d",
"document": "docs/clean-architecture-backend-template/final/document.md",
"document_sha256": "7c986b30b6ef3c12060b6749ee60d53e37d6994493d2703419732c9cab6077d8",
"anchor": {
"kind": "line",
"value": 13463,
"line": 13463
"value": 13469,
"line": 13469
}
},
"outputs": [
"redis-admission-stages.svg",
"redis-admission-stages.drawio",
"redis-admission-stages.mmd",
"redis-admission-stages.d2",
"redis-admission-stages.dot",
"redis-admission-stages.excalidraw",
"redis-admission-stages.alt.md"
],
"lint_issue_count": 0,
@@ -0,0 +1,15 @@
%% 의도된 guarded path와 실제 direct gateway 우회
%% question: Redis 명령은 guard를 통과하는가, semantic adapter에서 gateway로 우회하는가?
flowchart LR
n0["guarded command path"]
n1["CommandPolicyGuard"]
n2["승인 후 실행"]
n3["fail-closed 거절"]
n4["semantic adapters ×5"]
n5["RedisCommandGateway 직접 호출"]
n6["guard stages 우회"]
n0 -->|"admission"| n1
n1 -->|"통과"| n2
n1 -->|"거절"| n3
n4 -->|"lease.gateway()"| n5
n5 -->|"guard 미경유"| n6
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="319" viewBox="0 0 680 319" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">명령 입장의 단일 지점</title>
<desc id="diagram-description">SSOT 는 이 구조의 바닥이 카탈로그가 미분류 명령을 fail-closed 로 거부하는 것이라고 적는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;redis-admission-stages&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/clean-architecture-backend-template/final/document.md&quot;,&quot;document_sha256&quot;:&quot;8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;line&quot;,&quot;value&quot;:13463,&quot;line&quot;:13463}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<svg xmlns="http://www.w3.org/2000/svg" width="1055" height="465" viewBox="0 0 1055 465" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">의도된 guarded path와 실제 direct gateway 우회</title>
<desc id="diagram-description">CommandPolicyGuard는 guarded command path의 admission 지점이다. 그러나 현재 semantic adapter 다섯은 RedisLease에서 gateway를 직접 얻어 호출하므로 catalog, permit, slot, budget, translation, observation 단계가 이 경로에 적용되지 않는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;redis-admission-stages&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/clean-architecture-backend-template/final/document.md&quot;,&quot;document_sha256&quot;:&quot;7c986b30b6ef3c12060b6749ee60d53e37d6994493d2703419732c9cab6077d8&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;line&quot;,&quot;value&quot;:13469,&quot;line&quot;:13469}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
@@ -49,27 +49,52 @@
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="680" height="319" />
<polyline class="edge kind-request style-solid emphasis-primary" points="230.0,176.0 310.0,176.0 310.0,238.5 390.0,238.5" data-evidence="13463-13490" />
<rect class="edge-label-bg" x="312.0" y="193.2" width="44.0" height="22" rx="3" />
<text class="edge-label" x="334.0" y="208.2">통과</text>
<polyline class="edge kind-request style-dashed emphasis-normal" points="230.0,158.0 310.0,158.0 310.0,95.5 390.0,95.5" data-evidence="13463-13497" />
<rect class="edge-label-bg" x="312.0" y="112.8" width="44.0" height="22" rx="3" />
<text class="edge-label" x="334.0" y="127.8">거절</text>
<rect class="canvas" width="1055" height="465" />
<polyline class="edge kind-request style-dashed emphasis-normal" points="244.0,308.0 324.0,308.0 324.0,308.0 404.0,308.0" data-evidence="13469-13503" />
<rect class="edge-label-bg" x="264.8" y="266.0" width="118.5" height="22" rx="3" />
<text class="edge-label" x="324.0" y="281.0">lease.gateway()</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="244.0,172.0 331.0,172.0 331.0,167.0 418.0,167.0" data-evidence="13469-13503" />
<rect class="edge-label-bg" x="315.9" y="155.5" width="78.3" height="22" rx="3" />
<text class="edge-label" x="355.0" y="170.5">admission</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="578.0,176.0 692.0,176.0 692.0,228.0 806.0,228.0" data-evidence="13469-13503" />
<rect class="edge-label-bg" x="694.0" y="188.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="716.0" y="203.0">통과</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="578.0,158.0 692.0,158.0 692.0,92.0 806.0,92.0" data-evidence="13469-13503" />
<rect class="edge-label-bg" x="694.0" y="111.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="716.0" y="126.0">거절</text>
<polyline class="edge kind-data style-dashed emphasis-primary" points="592.0,308.0 672.0,308.0 672.0,376.0 752.0,376.0" data-evidence="13469-13503" />
<rect class="edge-label-bg" x="656.9" y="328.0" width="78.3" height="22" rx="3" />
<text class="edge-label" x="696.0" y="343.0">guard 미경유</text>
<g id="node-typed-entry">
<rect class="node-shape kind-service emphasis-normal role-source" data-evidence="13469-13503" x="70.0" y="140.0" width="174.0" height="64.0" rx="7" />
<text class="node-label" x="157.0" y="170.0">guarded command path</text>
</g>
<g id="node-semantic">
<rect class="node-shape kind-service emphasis-normal role-source" data-evidence="13469-13503" x="70.0" y="276.0" width="174.0" height="64.0" rx="7" />
<text class="node-label" x="157.0" y="306.0">semantic adapters ×5</text>
</g>
<g id="node-guard">
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="13463-13490" x="70.0" y="135.0" width="160.0" height="64.0" rx="7" />
<text class="node-label" x="150.0" y="165.0">CommandPolicyGuard</text>
<rect class="node-shape kind-service emphasis-primary role-service" data-evidence="13469-13503" x="418.0" y="135.0" width="160.0" height="64.0" rx="7" />
<text class="node-label" x="498.0" y="165.0">CommandPolicyGuard</text>
</g>
<g id="node-gateway">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="13469-13503" x="404.0" y="271.0" width="188.0" height="74.0" rx="7" />
<text class="node-label" x="498.0" y="298.0">RedisCommandGateway 직접</text>
<text class="node-label" x="498.0" y="316.0">호출</text>
</g>
<g id="node-deny">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="13463-13497" x="390.0" y="60.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="465.0" y="87.0">fail-closed 거절</text>
<line class="node-detail-divider" x1="404.0" y1="108.0" x2="526.0" y2="108.0" />
<text class="node-detail" x="406.0" y="125.0">미분류 · BLOCKED</text>
<rect class="node-shape kind-result emphasis-normal role-sink" data-evidence="13469-13503" x="806.0" y="60.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="881.0" y="90.0">fail-closed 거절</text>
</g>
<g id="node-run">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="13463-13490" x="390.0" y="203.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="465.0" y="230.0">실행</text>
<line class="node-detail-divider" x1="404.0" y1="251.0" x2="526.0" y2="251.0" />
<text class="node-detail" x="406.0" y="268.0">카탈로그 통과</text>
<rect class="node-shape kind-result emphasis-normal role-sink" data-evidence="13469-13503" x="806.0" y="196.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="881.0" y="226.0">승인 후 실행</text>
</g>
<g id="node-bypass">
<rect class="node-shape kind-result emphasis-primary role-sink" data-evidence="13469-13503" x="752.0" y="332.0" width="258.0" height="88.0" rx="7" />
<text class="node-label" x="881.0" y="359.0">guard stages 우회</text>
<line class="node-detail-divider" x1="766.0" y1="380.0" x2="996.0" y2="380.0" />
<text class="node-detail" x="768.0" y="397.0">catalog · permit · slot · budget</text>
<text class="node-detail" x="768.0" y="413.0">translation · observation</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

@@ -1,22 +1,22 @@
# 격리가 성립하는 세 조건
# PostgreSQL RLS 적용 여부를 가르는 분기
## Alternative text
격리 판정이 ENABLE RLS 와 FORCE RLS 와 BYPASSRLS 없는 롤을 차례로 확인하는 순서.
RLS 비활성은 policy 미적용으로, superuser와 BYPASSRLS는 우회로, owner는 FORCE 여부로 갈리고, policy 대상인데 applicable policy가 없으면 default deny가 되는 흐름도
## Long description
SSOT 는 RlsPolicyVerifier.requireEnforced 가 런타임 롤의 BYPASSRLS 를 확인하고 current_schema() 의 실제 테이블을 순회하며 tenant-scoped 목록에 든 것만 검사한다고 적는다.
PostgreSQL RLS를 세 개의 동시 전제로 보지 않는다. RLS가 활성화된 뒤 superuser 또는 BYPASSRLS인지, table owner인지와 FORCE RLS 여부를 확인한다. policy 대상 role에 applicable policy가 없으면 default deny이고, policy가 있으면 USING과 WITH CHECK를 평가한다.
## Elements and evidence
- **격리 판정** (service): No additional description. Evidence: L7396L7420.
- **RlsPolicyVerifier** (service): No additional description. Evidence: L7396L7420.
- **데이터베이스** (database): No additional description. Evidence: L7396L7426.
- **Boundary: policy 적용 대상 판정** (boundary): No additional description. Evidence: L7400L7432.
- **Boundary: policy 존재와 평가** (boundary): No additional description. Evidence: L7400L7432.
- **RLS 활성 여부** (process): No additional description. Evidence: L7400L7432.
- **policy 적용 대상** (process): No additional description. Evidence: L7400L7432.
- **applicable policy** (process): No additional description. Evidence: L7400L7432.
## Relationships
- **격리 판정 → RlsPolicyVerifier:** 검증 요청. Evidence: L7396L7420.
- **RlsPolicyVerifier → 데이터베이스:** ENABLE RLS 확인. Evidence: L7396L7420.
- **RlsPolicyVerifier → 데이터베이스:** FORCE RLS 확인. Evidence: L7396L7420.
- **RlsPolicyVerifier → 데이터베이스:** BYPASSRLS 없음 확인. Evidence: L7396L7426.
- **RLS 활성 여부 → policy 적용 대상:** RLS on. Evidence: L7400L7432.
- **policy 적용 대상 → applicable policy:** 적용. Evidence: L7400L7432.
@@ -1,16 +1,18 @@
# 격리가 성립하는 세 조건
# Question: RLS 격리는 무엇이 동시에 참이어야 성립하는가?
direction: right
n0: "격리 판정" {
shape: rectangle
# PostgreSQL RLS 적용 여부를 가르는 분기
# Question: 현재 role과 table에서 RLS policy가 실제로 적용되는가?
direction: down
g0: "policy 적용 대상 판정" {
n0: "RLS 활성 여부" {
shape: rectangle
}
n1: "policy 적용 대상" {
shape: rectangle
}
}
n1: "RlsPolicyVerifier" {
shape: rectangle
g1: "policy 존재와 평가" {
n2: "applicable policy" {
shape: rectangle
}
}
n2: "데이터베이스" {
shape: sql_table
}
n0 -> n1: "검증 요청"
n1 -> n2: "ENABLE RLS 확인"
n1 -> n2: "FORCE RLS 확인"
n1 -> n2: "BYPASSRLS 없음 확인"
g0.n0 -> g0.n1: "RLS on"
g0.n1 -> g1.n2: "적용"
@@ -0,0 +1,20 @@
digraph techviz {
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
subgraph cluster_0 {
label="policy 적용 대상 판정";
style="rounded,dashed";
color="#66788a";
n0 [label="RLS 활성 여부", shape=box, style="rounded,filled"];
n1 [label="policy 적용 대상", shape=box, style="rounded,filled"];
}
subgraph cluster_1 {
label="policy 존재와 평가";
style="rounded,dashed";
color="#66788a";
n2 [label="applicable policy", shape=box, style="rounded,filled"];
}
n0 -> n1 [label="RLS on", style=solid];
n1 -> n2 [label="적용", style=solid];
}
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="rls-three-preconditions" name="PostgreSQL RLS 적용 여부를 가르는 분기">
<mxGraphModel dx="860" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="860" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="g_role" value="policy 적용 대상 판정" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="45.0" y="49.0" width="470.0" height="177.0" as="geometry"/>
</mxCell>
<mxCell id="g_policy-zone" value="policy 존재와 평가" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="565.0" y="49.0" width="250.0" height="160.0" as="geometry"/>
</mxCell>
<mxCell id="n_rls" value="RLS 활성 여부&lt;br/&gt;no → policy 미적용" tooltip="process | Evidence: L7400-L7432" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="75.0" y="95.0" width="190.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_subject" value="policy 적용 대상&lt;br/&gt;superuser / BYPASSRLS → 우회&lt;br/&gt;owner + FORCE off → 우회&lt;br/&gt;non-owner 또는 owner + FORCE on → 대상" tooltip="process | Evidence: L7400-L7432" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="295.0" y="95.0" width="190.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_policy" value="applicable policy&lt;br/&gt;none → default deny&lt;br/&gt;exists → USING / WITH CHECK 평가" tooltip="process | Evidence: L7400-L7432" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="595.0" y="95.0" width="190.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="e_e1" value="RLS on" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_rls" target="n_subject">
<mxGeometry relative="1" as="geometry">
<mxPoint x="271.5" y="31.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_e2" value="적용" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_subject" target="n_policy">
<mxGeometry relative="1" as="geometry">
<mxPoint x="564.0" y="143.2" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,564 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "group-role",
"type": "rectangle",
"x": 45.0,
"y": 49.0,
"width": 470.0,
"height": 177.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#f8f9fa",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1777472244,
"version": 1,
"versionNonce": 1516900995,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "group-label-role",
"type": "text",
"x": 61.0,
"y": 55.0,
"width": 135,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 715621186,
"version": 1,
"versionNonce": 87917954,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 14,
"fontFamily": 5,
"text": "policy 적용 대상 판정",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "policy 적용 대상 판정",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "group-policy-zone",
"type": "rectangle",
"x": 565.0,
"y": 49.0,
"width": 250.0,
"height": 160.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#f8f9fa",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 169725814,
"version": 1,
"versionNonce": 1032311414,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "group-label-policy-zone",
"type": "text",
"x": 581.0,
"y": 55.0,
"width": 117,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1265739546,
"version": 1,
"versionNonce": 1075054467,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 14,
"fontFamily": 5,
"text": "policy 존재와 평가",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "policy 존재와 평가",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-e1",
"type": "arrow",
"x": 265.0,
"y": 59.0,
"width": 30.0,
"height": 88.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 71197658,
"version": 1,
"versionNonce": 618174464,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
71.5
],
[
30.0,
71.5
],
[
30.0,
0.0
],
[
0.0,
0.0
],
[
0.0,
88.5
],
[
30.0,
88.5
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-rls",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-subject",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-e1",
"type": "text",
"x": 226.5,
"y": 19.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 246258267,
"version": 1,
"versionNonce": 768010045,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "RLS on",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "RLS on",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-e2",
"type": "arrow",
"x": 485.0,
"y": 139.0,
"width": 110.0,
"height": 8.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 804950317,
"version": 1,
"versionNonce": 629875261,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
8.5
],
[
55.0,
8.5
],
[
55.0,
0.0
],
[
110.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-subject",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-policy",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-e2",
"type": "text",
"x": 519.0,
"y": 131.25,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 340809808,
"version": 1,
"versionNonce": 1429402641,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "적용",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "적용",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-rls",
"type": "rectangle",
"x": 75.0,
"y": 95.0,
"width": 190.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 46671207,
"version": 1,
"versionNonce": 1454808113,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-rls",
"type": "text",
"x": 85.0,
"y": 105.0,
"width": 170.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1139089945,
"version": 1,
"versionNonce": 19559202,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "RLS 활성 여부\nno → policy 미적용",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "RLS 활성 여부\nno → policy 미적용",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-subject",
"type": "rectangle",
"x": 295.0,
"y": 95.0,
"width": 190.0,
"height": 105.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 809707623,
"version": 1,
"versionNonce": 308999550,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-subject",
"type": "text",
"x": 305.0,
"y": 105.0,
"width": 170.0,
"height": 85.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 651634604,
"version": 1,
"versionNonce": 1863011863,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "policy 적용 대상\nsuperuser / BYPASSRLS → 우회\nowner + FORCE off → 우회\nnon-owner 또는 owner + FORCE on → 대상",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "policy 적용 대상\nsuperuser / BYPASSRLS → 우회\nowner + FORCE off → 우회\nnon-owner 또는 owner + FORCE on → 대상",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-policy",
"type": "rectangle",
"x": 595.0,
"y": 95.0,
"width": 190.0,
"height": 88.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1953932615,
"version": 1,
"versionNonce": 1148359251,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-policy",
"type": "text",
"x": 605.0,
"y": 105.0,
"width": 170.0,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1771766625,
"version": 1,
"versionNonce": 252892369,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "applicable policy\nnone → default deny\nexists → USING / WITH CHECK 평가",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "applicable policy\nnone → default deny\nexists → USING / WITH CHECK 평가",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -2,27 +2,31 @@
"harness_version": "0.2.0",
"spec_id": "rls-three-preconditions",
"spec_version": "1.1",
"spec_sha256": "f6268a244e162b30af9d251df67dbe2d8d3e23e84198f91656f865e0f5f96a1c",
"spec_sha256": "f49733b25ed5a35fa7ebbe452adbf1a606aad7108643d443b43a9fae25d15ebf",
"source_context": {
"document": "/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/clean-architecture-backend-template/final/document.md",
"document_sha256": "8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d",
"document": "docs/clean-architecture-backend-template/final/document.md",
"document_sha256": "7c986b30b6ef3c12060b6749ee60d53e37d6994493d2703419732c9cab6077d8",
"anchor": {
"kind": "line",
"value": 7398,
"line": 7398
"value": 7400,
"line": 7400
}
},
"outputs": [
"rls-three-preconditions.svg",
"rls-three-preconditions.drawio",
"rls-three-preconditions.mmd",
"rls-three-preconditions.d2",
"rls-three-preconditions.dot",
"rls-three-preconditions.excalidraw",
"rls-three-preconditions.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "sequence",
"composition_profile": "two-zone-pipeline",
"reference_ids": [
"payment-approval-sequence"
"localization-pipeline"
],
"diagram_only": true
}
@@ -0,0 +1,12 @@
%% PostgreSQL RLS 적용 여부를 가르는 분기
%% question: 현재 role과 table에서 RLS policy가 실제로 적용되는가?
flowchart TB
subgraph g_role["policy 적용 대상 판정"]
n0["RLS 활성 여부"]
n1["policy 적용 대상"]
end
subgraph g_policy_zone["policy 존재와 평가"]
n2["applicable policy"]
end
n0 -->|"RLS on"| n1
n1 -->|"적용"| n2
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="418" viewBox="0 0 680 418" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">격리가 성립하는 세 조건</title>
<desc id="diagram-description">SSOT 는 RlsPolicyVerifier.requireEnforced 가 런타임 롤의 BYPASSRLS 를 확인하고 current_schema() 의 실제 테이블을 순회하며 tenant-scoped 목록에 든 것만 검사한다고 적는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;rls-three-preconditions&quot;,&quot;profile&quot;:&quot;sequence&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/clean-architecture-backend-template/final/document.md&quot;,&quot;document_sha256&quot;:&quot;8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;line&quot;,&quot;value&quot;:7398,&quot;line&quot;:7398}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<svg xmlns="http://www.w3.org/2000/svg" width="860" height="300" viewBox="0 0 860 300" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">PostgreSQL RLS 적용 여부를 가르는 분기</title>
<desc id="diagram-description">PostgreSQL RLS를 세 개의 동시 전제로 보지 않는다. RLS가 활성화된 뒤 superuser 또는 BYPASSRLS인지, table owner인지와 FORCE RLS 여부를 확인한다. policy 대상 role에 applicable policy가 없으면 default deny이고, policy가 있으면 USING과 WITH CHECK를 평가한다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;rls-three-preconditions&quot;,&quot;profile&quot;:&quot;two-zone-pipeline&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/clean-architecture-backend-template/final/document.md&quot;,&quot;document_sha256&quot;:&quot;7c986b30b6ef3c12060b6749ee60d53e37d6994493d2703419732c9cab6077d8&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;line&quot;,&quot;value&quot;:7400,&quot;line&quot;:7400}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
@@ -49,26 +49,38 @@
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="680" height="418" />
<rect class="node-shape kind-service emphasis-normal role-participant" data-evidence="7396-7420" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="120.0" y="65.0">격리 판정</text>
<line class="lifeline" x1="120.0" y1="99.0" x2="120.0" y2="388.0" />
<rect class="node-shape kind-service emphasis-primary role-participant" data-evidence="7396-7420" x="255.0" y="35.0" width="153.0" height="64.0" rx="7" />
<text class="node-label" x="331.5" y="65.0">RlsPolicyVerifier</text>
<line class="lifeline" x1="331.5" y1="99.0" x2="331.5" y2="388.0" />
<rect class="node-shape kind-database emphasis-normal role-participant" data-evidence="7396-7426" x="465.0" y="48.0" width="150.0" height="52.0" /><ellipse class="node-shape kind-database emphasis-normal role-participant" cx="540.0" cy="48.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 465.0 100.0 A 75.0 13.0 0 0 0 615.0 100.0" />
<text class="node-label" x="540.0" y="75.0">데이터베이스</text>
<line class="lifeline" x1="540.0" y1="113.0" x2="540.0" y2="388.0" />
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 331.5,140.0" data-evidence="7396-7420" />
<rect class="edge-label-bg" x="189.9" y="114.0" width="71.6" height="22" rx="3" />
<text class="edge-label" x="225.8" y="129.0">1. 검증 요청</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="331.5,202.0 540.0,202.0" data-evidence="7396-7420" />
<rect class="edge-label-bg" x="373.1" y="176.0" width="125.2" height="22" rx="3" />
<text class="edge-label" x="435.8" y="191.0">2. ENABLE RLS 확인</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="331.5,264.0 540.0,264.0" data-evidence="7396-7420" />
<rect class="edge-label-bg" x="376.5" y="238.0" width="118.5" height="22" rx="3" />
<text class="edge-label" x="435.8" y="253.0">3. FORCE RLS 확인</text>
<polyline class="edge kind-request style-solid emphasis-primary" points="331.5,326.0 540.0,326.0" data-evidence="7396-7426" />
<rect class="edge-label-bg" x="366.4" y="300.0" width="138.6" height="22" rx="3" />
<text class="edge-label" x="435.8" y="315.0">4. BYPASSRLS 없음 확인</text>
<rect class="canvas" width="860" height="300" />
<rect class="group-box" x="45.0" y="49.0" width="470.0" height="177.0" rx="8" />
<rect class="group-label-bg" x="59.0" y="39.0" width="127.0" height="22" />
<text class="group-label" x="69.0" y="54.0">policy 적용 대상 판정</text>
<rect class="group-box" x="565.0" y="49.0" width="250.0" height="160.0" rx="8" />
<rect class="group-label-bg" x="579.0" y="39.0" width="113.0" height="22" />
<text class="group-label" x="589.0" y="54.0">policy 존재와 평가</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="265.0,130.5 295.0,130.5 295.0,59.0 265.0,59.0 265.0,147.5 295.0,147.5" data-evidence="7400-7432" />
<rect class="edge-label-bg" x="242.4" y="17.0" width="58.2" height="22" rx="3" />
<text class="edge-label" x="271.5" y="32.0">RLS on</text>
<polyline class="edge kind-request style-solid emphasis-primary" points="485.0,147.5 540.0,147.5 540.0,139.0 595.0,139.0" data-evidence="7400-7432" />
<rect class="edge-label-bg" x="542.0" y="129.2" width="44.0" height="22" rx="3" />
<text class="edge-label" x="564.0" y="144.2">적용</text>
<g id="node-rls">
<rect class="node-shape kind-process emphasis-normal role-stage" data-evidence="7400-7432" x="75.0" y="95.0" width="190.0" height="71.0" rx="7" />
<text class="node-label" x="170.0" y="122.0">RLS 활성 여부</text>
<line class="node-detail-divider" x1="89.0" y1="143.0" x2="251.0" y2="143.0" />
<text class="node-detail" x="91.0" y="160.0">no → policy 미적용</text>
</g>
<g id="node-subject">
<rect class="node-shape kind-process emphasis-normal role-stage" data-evidence="7400-7432" x="295.0" y="95.0" width="190.0" height="105.0" rx="7" />
<text class="node-label" x="390.0" y="122.0">policy 적용 대상</text>
<line class="node-detail-divider" x1="309.0" y1="143.0" x2="471.0" y2="143.0" />
<text class="node-detail" x="311.0" y="160.0">superuser / BYPASSRLS → 우회</text>
<text class="node-detail" x="311.0" y="176.0">owner + FORCE off → 우회</text>
<text class="node-detail" x="311.0" y="192.0">non-owner 또는 owner + FORCE on → 대상</text>
</g>
<g id="node-policy">
<rect class="node-shape kind-process emphasis-primary role-stage" data-evidence="7400-7432" x="595.0" y="95.0" width="190.0" height="88.0" rx="7" />
<text class="node-label" x="690.0" y="122.0">applicable policy</text>
<line class="node-detail-divider" x1="609.0" y1="143.0" x2="771.0" y2="143.0" />
<text class="node-detail" x="611.0" y="160.0">none → default deny</text>
<text class="node-detail" x="611.0" y="176.0">exists → USING / WITH CHECK 평가</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

@@ -721,16 +721,20 @@ public InboxCleanupJob inboxCleanupJob(...)
`JdbcInboxRepository`)을 **어떤 자동설정도 만들지 않는다.** 19 main 파일 / 2,818 LOC가 전부 조용히
비어 있다.
**실패가 특히 조용하다.** Spring은 조건부 bean이 조건을 만족하지 못하는 것을 오류로 보고하지
않는다. 즉 **"outbox가 꺼져 있음"과 "outbox가 조립될 수 없음"이 런타임에서 구별되지 않는다.**
**실패가 애플리케이션 오류나 health failure로 자동 승격되지는 않는다.** 따라서 기능을 호출하지 않는 한
"outbox가 꺼져 있음"과 "outbox bean이 조건 불일치로 생성되지 않음"이 제품 동작에서는 비슷하게 보일 수 있다.
다만 condition evaluation evidence가 사라지는 것은 아니다. Spring Boot의 `ConditionEvaluationReport`와,
endpoint가 노출된 경우 `/actuator/conditions`에서 positive/negative match와 이유를 확인할 수 있다.
같은 starter가 `MessageCodecRegistry`에는 `@ConditionalOnMissingBean` 기본 구현을 제공했다는 점이
것을 결함으로 만든다.
조립 차이를 검토할 이유다.
**마이그레이션 스트림을 적용하는 곳이 없고, 적용하려는 순간 버전이 충돌한다** (`19` §7.2).
합성 루트의 Flyway 기본 위치는 `PostgreSqlPersistenceConfig:115`
`classpath:db/migration/postgresql`고, `db/migration/messaging` 이름으로 부르는 main 코드가
저장소 전체에 **0건**이다. 그리고 두 leaf가 같은 리소스 디렉터리에 각자 번호를 매긴다:
`classpath:db/migration/postgresql`다. searched direct reference 기준으로 `db/migration/messaging`
이름으로 지정하는 main 코드는 찾지 못했다. 이 결과는 직접 지정 코드가 검색되지 않았다는 뜻이며,
외부 설정·resource scanning·reflection·framework convention까지 포함해 runtime 사용이 없음을 단독으로 증명하지는 않는다.
두 leaf가 같은 리소스 디렉터리에 각자 번호를 매긴다는 사실은 별개로 유지된다:
```
messaging-inbox-jdbc-postgresql V2__messaging_inbox.sql (CREATE TABLE)
@@ -7397,6 +7401,8 @@ Evidence: `evidence/raw/096-experimental-gate-reachability.txt`, `099-experiment
`RlsPolicyVerifier.requireEnforced(runtimeDataSource, tenantScopedTables)`의 이름과 Javadoc은 caller가 지정한 tenant-scoped table들이 실제로 RLS에 의해 보호되는지 증명하는 contract다. 구현은 runtime role의 `BYPASSRLS`를 확인하고, `current_schema()`의 실제 table들을 순회하면서 이름이 `tenantScopedTables`에 포함된 row만 검사한다.
여기서 PostgreSQL 의미를 분리해서 읽어야 한다. RLS가 꺼져 있으면 policy가 적용되지 않는다. RLS가 켜져 있고 현재 role에 적용 가능한 policy가 없으면 일반 role에는 **default deny**가 적용된다. superuser와 `BYPASSRLS` role은 RLS를 우회한다. table owner도 기본적으로 우회하지만 `FORCE ROW LEVEL SECURITY`를 켜면 owner는 policy 대상이 된다. `FORCE`가 superuser나 `BYPASSRLS`의 우회를 없애는 것은 아니다. 따라서 이 값들을 항상 동시에 참이어야 하는 ‘세 전제’로 묶지 않는다.
문제는 반대 방향 검증이 없다는 것이다. 즉 caller가 요구한 table 이름이 실제 catalog 결과에 **한 번도 등장하지 않아도** 성공한다.
```text
@@ -11438,7 +11444,7 @@ PROBE NUL byte then <html> -> ACCEPT / NO_SCRIPTABLE_CONTENT ←
PROBE plain text -> ACCEPT / NO_SCRIPTABLE_CONTENT
```
세 가지가 통과한다. `String.stripLeading()``Character.isWhitespace`만 제거하므로 **UTF-8 BOM(U+FEFF)도 NUL도 지우지 않고**, 선행 HTML 주석은 어떤 마커로도 시작하지 않는다. 셋 다 브라우저는 HTML로 렌더링한다 — BOM 접두 HTML은 이국적인 우회가 아니라 여러 편집기의 기본 출력이다.
세 가지가 detector를 통과한다. `String.stripLeading()``Character.isWhitespace`만 제거하므로 **UTF-8 BOM(U+FEFF)도 NUL도 지우지 않고**, 선행 HTML 주석은 어떤 마커로도 시작하지 않는다. 이 probe가 증명한 범위는 detector bypass까지다. 실제 대상 브라우저가 각 입력을 실행 가능한 콘텐츠로 해석하는지는 이번 evidence에서 확인하지 않았다.
형제 검증기와의 대비가 판정을 굳힌다. `MediaTypeVerifier`는 매직바이트를 접두사 **시작**에서 비교하는데, 그것은 시그니처의 정의가 파일 선두이므로 옳다. scriptable 마커는 시그니처가 아니라 **브라우저가 스니핑하는 패턴**이고, 브라우저는 선두 고정 매칭을 하지 않는다. 같은 "접두사 시작 비교"가 한쪽에서는 정확하고 다른 쪽에서는 우회 가능하다.
@@ -11485,7 +11491,7 @@ PROBE plain text -> ACCEPT / NO_SCRIPTABLE_CONTENT
| 우선순위 | finding | reachability |
|---|---|---|
| **P2** | README:105 "No setting or bean for those capabilities is exposed"가 audit·health·reaping·quota 네 능력에 대해 사실과 다르다 — 8개 port 구현과 app-bootstrap의 8개 bean으로 확정 | 이 문단으로 능력 유무를 판단하는 독자 |
| **P2** | `ScriptableContentPolicy`가 마커를 접두사 **시작**에서만 찾아, UTF-8 BOM·NUL·선행 HTML 주석이 붙은 실행 가능 콘텐츠를 ACCEPT한다 (실행 probe 3건) | `inlineSafeProfile=false`이고 claimed 타입을 선언하지 않는 업로드 |
| **P2** | `ScriptableContentPolicy`가 마커를 접두사 **시작**에서만 찾아, UTF-8 BOM·NUL·선행 HTML 주석이 붙은 입력을 ACCEPT한다 (detector probe 3건). 실제 브라우저 실행 가능성은 별도 검증하지 않음 | `inlineSafeProfile=false`이고 claimed 타입을 선언하지 않는 업로드 |
| **P3/기록** | 실패 분류가 예외 메시지 텍스트("stale file handle", "timed out", "No space left on device")에 의존한다 — 문구가 달라지면 보수적 기본값으로 떨어지므로 안전한 방향 | 로케일/JDK 판본이 다른 배포 |
#### 46. Sub-scope 05 완료 조건
@@ -13467,7 +13473,7 @@ RedisEphemeralFanoutAdapter implements EphemeralFanoutPort
> `CommandPolicyGuard`: "**The single admission point every command passes through.**"
> `RedisCommandGateway`: "Policy, permits, budgets, timeouts, and observability are not this interface's concern: **everything routed through it has already passed `CommandPolicyGuard`**."
의미 어댑터 다섯은 그 전제를 만족하지 않는다(`165-...` §8.1).
이 문장은 현재 runtime 전체의 사실이 아니라 **의도된 guarded command path의 계약**으로 읽어야 한다. 의미 어댑터 다섯은 그 전제를 만족하지 않는다(`165-...` §8.1). 따라서 이후 admission 단계 설명도 guard를 통과하는 경로에 한정한다.
- `SyncRedisCommandExecutor`·`ReactiveRedisCommandExecutor`·`CommandPolicyGuard`·`CommandRequest`를 참조하는 파일 **0**(exit=1)
- 타입 있는 API(`RedisValueOperations`·`RedisHashOperations`·`RedisKeyOperations`·`RedisOperations`)를 참조하는 파일 **0**(exit=1)
@@ -17555,11 +17561,11 @@ private ExternalRequestContext externalRequest(ServerHttpRequest request) {
**권고** — 하나를 남긴다. `RequestLoggingFilter``WebMvcRequestIdFilter`가 요청 속성에 넣은 값을 읽게 하면(`WebMvcRequestIdFilter.requestId(request)`가 이미 그 접근자다) 정책이 한 곳에 남고 MDC·로그·응답 헤더가 일치한다.
##### 32.2 P2 — forwarded 헤더 신뢰 판정이 Nginx 설정에만 있고, 그것을 위해 쓴 Java 정책 421 LOC은 배선되지 않는
##### 32.2 P2 — 테스트 Nginx 설정은 forwarded 헤더를 교체하지만 운영 trust boundary와 Java 정책 wiring은 미확인이
`server.forward-headers-strategy=framework`(기본값)에서 Spring이 `X-Forwarded-Proto`·`X-Forwarded-Host`·`X-Forwarded-Port`·`X-Forwarded-Prefix`**보낸 피어가 누구든** 반영한다. 그 값이 `request.getURI()`를 바꾸고, 그것이 `ExternalRequestContext`가 되고(§31.4), 그것으로 `Location` 헤더와 페이지네이션 링크가 만들어진다.
스푸핑을 막는 것은 `nginxProxyTest` 레인이 증명하는 **Nginx 설정**이다:
`nginxProxyTest` 레인에서는 다음 **Nginx 설정**이 클라이언트가 보낸 forwarded 헤더를 교체한다:
```
NginxProxyContractIT:63 attackerCannotOverrideForwardedHost() X-Forwarded-Host: evil.example
@@ -17569,15 +17575,15 @@ NginxProxyContractIT:143 clientCannotInjectAPrefix()
// "X-Forwarded-Prefix is set per location, so a client's value is replaced."
```
이 보증 근거는 `nginxProxyTest/resources/nginx/proxy_headers.conf`가 location마다 헤더를 **덮어쓴다**는 사실이다. 애플리케이션 검사하지 않는다.
테스트 레인의 보증 근거는 `nginxProxyTest/resources/nginx/proxy_headers.conf`가 location마다 헤더를 **덮어쓴다**는 사실이다. 이 결과만으로 실제 운영 배포가 같은 설정을 사용한다고 보거나, 모든 운영 경로에서 애플리케이션 검사가 없다고 단정하지 않는다.
`TrustedProxyPolicy`(161줄, CIDR 기반 피어 허용목록)가 애플리케이션 쪽 검사를 위해 존재하고, 프로덕션에서 생성되지 않는다. testkit의 `ProxyFixtureController:53` `TrustedProxyPolicy.of("10.0.0.0/8", …)`를 직접 만들어 픽스처에 붙인다 — SS4·SS5와 같은 형태다.
`TrustedProxyPolicy`(161줄, CIDR 기반 피어 허용목록)가 애플리케이션 쪽 검사를 위해 존재한다. searched direct reference와 확인한 자동설정 경로에서는 production construction을 찾지 못했고, testkit의 `ProxyFixtureController:53` `TrustedProxyPolicy.of("10.0.0.0/8", …)`를 직접 만들어 픽스처에 붙인다. reflection·framework lifecycle·외부 조립까지 포함한 전체 runtime wiring 부재는 이번 evidence로 확정하지 않았다.
**실패 시나리오** — 배포가 그 Nginx 설정을 쓰지 않거나(다른 인그레스, 서비스 메시, k8s 내부에서 파드 IP로 직접 도달), 인그레스를 우회하는 경로가 하나라도 있으면, 클라이언트가 `X-Forwarded-Host: evil.example`을 보내 그 요청이 만드는 모든 절대 URL을 자기 도메인으로 돌린다. 비밀번호 재설정 링크나 `Location` 헤더가 그 URL을 담으면 그대로 피싱 벡터가 된다.
**조건부 실패 시나리오** — 운영 배포가 forwarded 헤더를 신뢰하면서도 앞단에서 값을 교체·검증하지 않는 경로가 있다면, 클라이언트가 `X-Forwarded-Host: evil.example` 같은 값을 주입해 절대 URL 생성에 영향을 줄 수 있다. 이번 검증은 그러한 운영 경로가 실제로 존재하는지까지 확인하지 않았다.
**이것을 방어로 쓰는 것 자체는 정당하다** — 인그레스에서 덮어쓰는 것이 표준 관행이다. 기록하는 것은 두 가지다: (1) 그 의존이 코드나 문서에 명시돼 있지 않고 레인의 `.conf` 파일에만 있다, (2) 애플리케이션 쪽 이중 방어로 쓰라고 421줄을 작성해 두고 연결지 않다.
**인그레스에서 forwarded 헤더를 authoritative value로 교체하는 설계 자체는 가능하다.** 이번 evidence가 확인한 것은 테스트 `.conf`의 교체 동작과 Java trust-policy 코드의 존재다. 실제 운영이 이 설정에 의존하는지, Java 정책이 운영 lifecycle에서 정말 연결지 않는지는 추가 조립·배포 evidence가 필요하다.
**권고** — `TrustedProxyPolicy``forward-headers-strategy` 앞단에 배선하거나(피어가 목록 밖이면 forwarded 헤더를 버린다), 최소한 README에 "이 플랫폼은 인그레스가 `X-Forwarded-*`를 덮어쓴다고 전제한다"를 명시하고 `proxy` 패키지를 제거한다. 지금 상태는 그 전제를 아무 데도 적지 않은 채 그것을 대체할 코드를 갖고 있다.
**권고** — 운영 trust boundary를 먼저 확정한다. 인그레스가 `X-Forwarded-*`를 authoritative value로 교체하는 구조라면 그 전제를 운영 문서와 계약 테스트에 명시한다. 애플리케이션에서도 피어 신뢰를 검증하려는 설계라면 `TrustedProxyPolicy`의 실제 lifecycle wiring을 확인하고 빠진 경로를 연결한다.
##### 32.3 P3/기록 — `ExternalRequestContext.prefix`가 항상 빈 문자열이고 `WebAuditPublisher`는 참조 0이다
@@ -18040,7 +18046,7 @@ web 쪽이 구조적으로 우월하다. `build.gradle`이 그 이유를 적는
| P2 | 24.1 | 배선된 `CacheControlFilter``no-store`가 배선된 조건부 읽기(ETag/304)를 무력화하고, 조정용 `cache` 패키지 310 LOC은 참조 0 |
| P2 | 28.1 | `maxArrayElements`가 선언만 되고 강제되지 않으며 바이트 예산 백스톱(§16.1)도 없다 |
| P2 | 32.1 | 요청 식별자를 클라이언트가 고를 수 없다는 정책이, 뒤에 도는 다른 배선 필터에 의해 뒤집힌다 |
| P2 | 32.2 | forwarded 헤더 신뢰 판정이 Nginx 설정에만 있고 Java 정책 421 LOC은 미배선 |
| P2 | 32.2 | 테스트 Nginx 설정은 forwarded 헤더를 교체한다. 운영 trust boundary와 Java 정책의 전체 lifecycle wiring은 이번 evidence로 확정하지 못함 |
| P2 | 36.1 | 선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다 |
| P3 ×9 | 8.2 · 20.3 · 24.3 · 25.3 · 25.4 · 36.2 · 44.1 · 44.2 · 28.3 외 | 죽은 메서드 · 구분자 기반 지문 · 미강제 한도 · 이름 불일치 · 참조 0인 138줄 · 실행되지 않는 시작 검증 등 |
@@ -22425,9 +22431,9 @@ they write inside the application's own transaction, against the application's o
configuration.locations("classpath:db/migration/postgresql");
```
조건부 스트림은 각자 자기 위치와 history table을 갖는다 — `NotificationSchemaStream.LOCATION = "classpath:db/migration/jpa/notification-platform"`, fileserver 스트림 등. **`db/migration/messaging`을 이름으로 부르는 main 코드는 저장소 전체에 0건이다.** 참조는 세 개의 IT(`InboxPostgresIT`, `OutboxPostgresIT`, `AdminOperationJournalPostgresIT`)가 자기 테스트 컨테이너에 직접 적용할 때뿐이다.
조건부 스트림은 각자 자기 위치와 history table을 갖는다 — `NotificationSchemaStream.LOCATION = "classpath:db/migration/jpa/notification-platform"`, fileserver 스트림 등. searched direct reference 기준으로 **`db/migration/messaging`을 이름으로 지정하는 main 코드는 찾지 못했다.** 검색된 참조는 세 개의 IT(`InboxPostgresIT`, `OutboxPostgresIT`, `AdminOperationJournalPostgresIT`)가 자기 테스트 컨테이너에 직접 적용하는 경로다.
`messaging_outbox` · `messaging_inbox` · admin operation journal 테이블은 **출하 배포 어디에서도 생성되지 않는다.** §7.1과 합치면 일관은 있다 — repository bean이 없으니 테이블도 필요 없다. 그러나 `persistence-jpa` leaf가 같은 모양의 결함을 세 번 고치고 그 이력을 javadoc에 남겨 두었다:
이 정적 검색만으로 외부 설정·resource scanning·reflection·framework convention을 모두 배제할 수는 없다. 따라서 여기서 확정할 수 있는 것은 출하 코드 안의 직접 wiring을 찾지 못했다는 범위까지다. repository bean이 현재 확인한 자동설정 경로에서 만들어지지 않는다는 §7.1 결과와 함께 보면 runtime assembly가 닫혀 있지 않다는 신호는 강하지만, ‘어떤 출하 배포에서도 테이블이 생성되지 않는다’고 일반화하지 않는다. 그러나 `persistence-jpa` leaf가 같은 모양의 결함을 세 번 고치고 그 이력을 javadoc에 남겨 두었다:
> "`PostgreSqlSameStoreInboxAdapter` ... its tables live only in `db/migration/jpa/inbox`. **The bean existed, its tables did not**, and the failure arrived either at ..."
> (같은 문장이 `PostgreSqlImmutableOutboxAppendAdapter`, `PostgreSqlPollingDeliveryAdapter`에도 있다)
@@ -22444,7 +22450,7 @@ messaging-outbox-jdbc-postgresql : V1__messaging_outbox.sql
V4__messaging_outbox_canonical_metadata.sql
```
**`V2`가 두 개다.** 두 jar가 한 classpath에 있고 Flyway가 `classpath:db/migration/messaging`스캔하면 "Found more than one migration with version 2"로 실패한다. 지금 실패하지 않는 유일한 이유는 (a) — 아무도 그 위치를 Flyway에 주지 않기 때문이다.
**`V2`가 두 개다.** 두 jar가 한 classpath에 있고 Flyway가 `classpath:db/migration/messaging`같은 location으로 스캔하면 duplicate version 오류가 된다. 현재 정적 검색에서는 그 location을 직접 지정하는 main 코드를 찾지 못했지만, 이것을 현재 실패하지 않는 유일한 이유’로 단정하지 않는다. framework/external configuration 경로는 이번 정적 검색으로 닫지 못했다.
각 leaf의 IT는 자기 jar의 리소스만 보므로 이 충돌을 재현하지 못한다 — `InboxPostgresIT:199``V2__messaging_inbox.sql`을 파일명으로 직접 읽고, `OutboxPostgresIT:249`는 자기 디렉터리를 나열한다. **두 leaf를 한 classpath에 올린 상태를 검증하는 테스트가 없다.**
@@ -23056,7 +23062,7 @@ grpc-spring-boot-starter/src/test/.../GrpcPlatformStartupValidatorTest.java (1
grpc-spring-boot-starter/src/main/.../GrpcPlatformStartupValidator.java (선언 자신)
```
**main 참조 0.** 클래스 `final` + `private` 생성자 + static 메서드(`violations(...)`, `requireValid(...)`)이므로 bean이 될 수도 없다 — 누군가 `requireValid`를 호출해야 하고, 호출하는 곳이 없다.
searched direct reference 기준으로 production caller를 찾지 못했다. 클래스 `final` + `private` 생성자 + static 메서드(`violations(...)`, `requireValid(...)`)인 것도 자동 bean 등록 경로가 아니라는 강한 신호다. 다만 direct reference 0만으로 reflection·framework discovery까지 전부 배제했다고 말하지 않는다. 실제 실행 여부는 assembly/lifecycle 경로와 boot evidence를 함께 확인해야 한다.
**실행되지 않는 규칙이 13개다.** validator 본문을 읽어 전수 확인했다:
@@ -23076,7 +23082,7 @@ grpc-spring-boot-starter/src/main/.../GrpcPlatformStartupValidator.java (
CLAUDE.md가 인용한 "streaming method가 Stable catalog에 등록되면 거부"는 methods 그룹의 네 번째 규칙(`!policy.rpcType().stable()`)이고, §2.2의 runtime 강제는 advanced isolation 그룹의 유일한 규칙이다. **둘 다 실행되지 않는다.**
이 형태는 이 저장소에서 네 번째다 — 모듈 14 §44.2(`WebPlatformStartupValidator`), 모듈 17 §4.1(`WebSocketPlatformStartupValidator`), 모듈 19 §3.5(`KafkaTransactionProfileValidator`), 그리고 여기. 그리고 모듈 18에서 확립한 규칙이 다시 성립한다 — **시작 검증기가 도는지 여부는 그 능력에 자동설정 루트가 있는지와 일치한다**. 여기서는 루트가 **있는데도** 검증기를 부르지 않는 첫 사례다.
이 형태는 이 저장소에서 반복해서 나타난다 — 모듈 14 §44.2(`WebPlatformStartupValidator`), 모듈 17 §4.1(`WebSocketPlatformStartupValidator`), 모듈 19 §3.5(`KafkaTransactionProfileValidator`), 그리고 여기. 여기서 일반화할 수 있는 규칙은 auto-configuration root 존재 여부와 실행 여부가 일치한다’가 아니다. **시작 검증기의 실행 여부는 direct caller뿐 아니라 `@Bean`/component scan/auto-configuration, lifecycle callback, event/post processor, framework discovery와 실제 boot evidence까지 따라가서 확인한다.** 이 사례에서는 확인한 auto-configuration이 검증기를 직접 부르지 않는다는 사실까지 확정했다.
**채택 시점 실패 시나리오.** 팀이 `runtime_memberships`에 런타임을 추가하고 `ca-skeleton.grpc.platform.enabled=true`로 켠다. Stable catalog에 client-streaming 메서드를 하나 등록한다(Stable 범위 밖이라는 것을 모른 채). 부팅은 성공한다. 그 메서드는 Stable이 보장하지 않는 경로로 실행되고, `grpc-advanced-streaming`의 세션·중복제거·체크포인트 기계는 조립돼 있지 않다. 거부했어야 할 검증기는 존재하고, 테스트도 12개 통과하며, 호출되지 않는다.
@@ -24404,6 +24410,8 @@ private static void appendField(StringBuilder canonical, String value) {
*/
```
위 javadoc의 one byte at a time 표현은 timing 공격의 위험을 설명하려는 문구지만, Java API가 보장하는 성질보다 강하게 읽지 않는다. 핵심은 입력 내용이나 common prefix에 따라 일찍 끝나는 비교를 피하고, JDK `MessageDigest.isEqual`이 문서화한 comparison timing property를 사용하는 것이다.
세 가지가 코드로 지켜진다.
```java
@@ -1,10 +1,10 @@
{
"assetKey": "rls-three-preconditions-diagram",
"kind": "diagram",
"svg": "assets/diagrams/rls-three-preconditions.svg",
"svg": "assets/diagrams/rls-three-preconditions/rls-three-preconditions.svg",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"svgSha256": "8c5d78987d966d8e464ea5574e4353a7db57052afef57512ef8dc75179f8f771",
"claim": "PostgreSQL RLS 가 실제로 격리하려면 정책 활성과 OWNER 강제와 런타임 롤의 BYPASSRLS 부재가 동시에 참이어야 한다",
"svgSha256": "e270994f7481a8709f4b8841f310464258535fd30e9cacbed5b66c6ae2c51093",
"claim": "PostgreSQL RLS 적용 여부는 RLS 활성 상태, superuser/BYPASSRLS 우회, table owner와 FORCE RLS, applicable policy 유무를 순서대로 구분하며 policy 대상 role에 applicable policy가 없으면 default deny가 적용된다",
"authoredAt": "2026-09-01T08:56:28+00:00",
"constraints": "상자 7개 이하 · 라벨은 이름 · 글자 2종 · 색 단독 의미 없음 · 숫자 없음"
}