Consolidate data access control backoffice updates

This commit is contained in:
devmrko
2026-07-13 23:06:23 +09:00
parent 403298d474
commit e18b30feab
181 changed files with 11571 additions and 954 deletions

File diff suppressed because it is too large Load Diff

63
docs/README.md Normal file
View File

@@ -0,0 +1,63 @@
# vpd-permission-poc 문서 아키텍처 (Documentation Map)
이 프로젝트의 문서는 **Diátaxis** 프레임워크 + **ADR** + **설계서(Design Spec)**
결합한 구조를 따른다. 모든 페르소나는 문서를 만들거나 참조할 때 이 지도를 기준으로 한다.
## 디렉토리 구조
```
docs/
README.md ← (이 파일) 문서 지도 · 인덱스
design/ ← 설계서: 구현 "전"에 작성하는 필수 산출물 (Design-First 게이트)
_TEMPLATE.md 기능 설계서 템플릿
_FN_TEMPLATE.md 함수별 설계서 템플릿
<issue-id>-<slug>/ 기능 1개(이슈 1개)당 폴더
README.md 기능 설계서 (전체 설계 + 함수 명세 표)
fn-<name>.md 복잡한 함수만 개별 함수 설계서
adr/ ← Architecture Decision Records: 가로지르는 결정 기록
_TEMPLATE.md
NNNN-<title>.md
reference/ ← 레퍼런스: 구현된 모듈/함수/설정 사양 (구현 "후" 동기화)
guides/ ← How-to / 사용 가이드 / 튜토리얼 (사용자·운영자 대상)
pipeline/ ← 개발 프로세스 문서 (큐 프로토콜·런북)
```
## Diátaxis 사분면 매핑
| 사분면 | 목적 | 여기서 위치 |
|--------|------|-------------|
| **Tutorials** (학습) | 처음 사용자가 따라하기 | `guides/` (getting-started) |
| **How-to** (문제해결) | 특정 작업 수행 | `guides/` |
| **Reference** (정보) | 정확한 사양 조회 | `reference/` |
| **Explanation** (이해) | 왜 이렇게 설계했나 | `design/`, `adr/` |
## 문서 종류와 책임
| 문서 | 작성 페르소나 | 시점 | 한 줄 |
|------|---------------|------|-------|
| 기능 설계서 `design/<id>/README.md` | **Architect** | 구현 **전** | 무엇을·어떻게 만들지의 청사진 |
| 함수 설계서 `design/<id>/fn-*.md` | **Architect** | 구현 **전** | 복잡 함수의 계약·알고리즘·테스트 |
| ADR `adr/NNNN-*.md` | **Architect** | 결정 시 | 되돌리기 어려운 선택과 근거 |
| 레퍼런스 `reference/*` | **Developer/Documenter** | 구현 **후** | 실제 코드 사양 |
| 가이드 `guides/*` | **Documenter** | 릴리스 시 | 사용/운영 방법 |
## 핵심 규칙 — Design-First (하드 게이트)
> **설계서 없이는 코드 없음.** 어떤 함수든 구현 전에 그 함수가 설계서로 덮여 있어야 한다
> (단순 함수: 기능 설계서의 함수 명세 표 / 복잡 함수: 개별 `fn-*.md`).
> Developer 는 설계서가 없으면 구현을 거부하고 Architect 단계로 반려한다.
> 자세한 기준은 `CLAUDE.md` §2 참조.
## 명명 · 추적성 규칙
- 설계서 폴더: `design/<issue-id>-<kebab-slug>/` (예: `design/45-trailing-stop/`).
- 함수 설계서: `fn-<function_name>.md` (예: `fn-calc_trailing_stop.md`).
- ADR: 4자리 일련번호 `adr/0001-<title>.md`, 번호 재사용 금지.
- 모든 설계서·ADR 상단에 **추적성 헤더**(Redmine 이슈, 관련 ADR, 구현 파일, 테스트)를 둔다.
- 코드 ↔ 설계서 양방향 링크: 설계서는 구현 파일 경로를, 코드 주석/문서는 설계서 경로를 가리킨다.
## 문서 수명주기
`Draft`(작성) → `Approved`(QA/Reviewer 통과 후) → `Superseded`(대체 시 상단 표기, 삭제 금지).
구현이 설계서와 달라지면 **코드가 아니라 설계서를 먼저 고치고** 다시 구현한다.
```

24
docs/adr/_TEMPLATE.md Normal file
View File

@@ -0,0 +1,24 @@
<!-- ADR 템플릿. 복사해서 adr/NNNN-<kebab-title>.md (4자리 일련번호). -->
# ADR-NNNN: <제목>
> **상태**: Proposed <!-- Proposed | Accepted | Superseded by ADR-XXXX -->
> **날짜**: <YYYY-MM-DD> · **결정자**: [AI] Architect · **관련 이슈**: #<id>
## 맥락 (Context)
무엇이 이 결정을 강제하는가. 배경·제약·요구.
## 결정 (Decision)
우리는 무엇을 하기로 했는가. (명확한 한 문단)
## 근거 (Rationale)
왜 이 선택인가. 핵심 트레이드오프.
## 결과 (Consequences)
- **긍정**: ...
- **부정 / 비용**: ...
- **후속 작업**: ...
## 검토한 대안 (Alternatives Considered)
- **<대안 A>** — 기각 사유: ...
- **<대안 B>** — 기각 사유: ...

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="620" viewBox="0 0 1120 620" role="img" aria-labelledby="title desc">
<title id="title">보안 인벤토리 확인 화면</title>
<desc id="desc">VPD, Redaction, DDS 권한 적용 현황을 확인하는 화면.</desc>
<rect width="1120" height="620" fill="#f4f6f8"/>
<rect x="34" y="30" width="1052" height="560" rx="10" fill="#1f2937"/>
<circle cx="68" cy="58" r="7" fill="#ef4444"/>
<circle cx="92" cy="58" r="7" fill="#f59e0b"/>
<circle cx="116" cy="58" r="7" fill="#22c55e"/>
<text x="150" y="64" fill="#d1d5db" font-family="Menlo, Consolas, monospace" font-size="18">ADMIN Inventory - Policy and Data Grant Matrix</text>
<text x="58" y="112" fill="#fde68a" font-family="Menlo, Consolas, monospace" font-size="20">=== VPD / Redaction 적용 객체 ===</text>
<text x="58" y="160" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">OBJECT_NAME POLICY ENABLE</text>
<line x1="58" y1="182" x2="1038" y2="182" stroke="#4b5563" stroke-width="2"/>
<text x="58" y="222" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">CB_V_SEARCH_DOCUMENTS CB_AGENT_DOC_POLICY YES</text>
<text x="58" y="262" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">CB_V_SEARCH_DOCUMENTS CB_CONTENTS_REDACT YES</text>
<text x="58" y="302" fill="#fde68a" font-family="Menlo, Consolas, monospace" font-size="20">=== DDS END USER -> DATA ROLE -> DATA GRANT ===</text>
<text x="58" y="342" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">cb_dds_hr CB_DDS_HR_ROLE dept_code='HR' EXCEPT CONTENTS</text>
<text x="58" y="382" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">cb_dds_fin CB_DDS_FIN_ROLE dept_code='FIN' EXCEPT CONTENTS</text>
<text x="58" y="422" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">cb_dds_all CB_DDS_ALL_ROLE 1=1 ALL COLUMNS</text>
<text x="58" y="462" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">cb_dds_none CONNECT_ONLY no DATA GRANT ORA-00942</text>
<text x="58" y="524" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">확인 SQL: DBA_POLICIES, REDACTION_POLICIES, DBA_DATA_GRANTS</text>
<text x="58" y="558" fill="#86efac" font-family="Menlo, Consolas, monospace" font-size="18">결과: 객체별 정책과 End User별 Grant를 한 번에 대조</text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="620" viewBox="0 0 1120 620" role="img" aria-labelledby="title desc">
<title id="title">Bearer Key ORDS 요청 화면</title>
<desc id="desc">ORDS Bearer Key 조회, 사용자 매핑, 필터링된 검색 결과 요약 화면.</desc>
<rect width="1120" height="620" fill="#f4f6f8"/>
<rect x="34" y="30" width="1052" height="560" rx="10" fill="#172033"/>
<circle cx="68" cy="58" r="7" fill="#ef4444"/>
<circle cx="92" cy="58" r="7" fill="#f59e0b"/>
<circle cx="116" cy="58" r="7" fill="#22c55e"/>
<text x="150" y="64" fill="#d1d5db" font-family="Menlo, Consolas, monospace" font-size="18">ORDS - Header Bearer Key 사용자 매핑</text>
<text x="58" y="112" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== 수신 요청 ===</text>
<text x="58" y="154" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">POST /ords/cb-ords/cb-agent-security/vpd/documents</text>
<text x="58" y="188" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">Authorization: Bearer cb_hr_key</text>
<text x="58" y="246" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== ORDS 처리 로직: 필수값 확인 ===</text>
<text x="58" y="288" fill="#fca5a5" font-family="Menlo, Consolas, monospace" font-size="18">Authorization Header 필수. 없으면 ORA-20101 또는 401/403</text>
<text x="58" y="336" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== ORDS 처리 로직: Key 매핑 ===</text>
<text x="58" y="376" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">1. Key Hash 계산 -> SHA256: 4A91...7C20</text>
<text x="58" y="410" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">2. CB_AGENT_BEARER_KEY 조회 -> USER_ID=101, KEY_ID=1</text>
<text x="58" y="444" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">3. Context reset + set -> EMP_NO=E10234, DEPT_CODE=HR</text>
<text x="58" y="496" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== 보호 객체 조회 ===</text>
<text x="58" y="532" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">CB_V_SEARCH_DOCUMENTS 조회 -> HR 행 3건, CONTENTS=NULL</text>
<text x="58" y="566" fill="#86efac" font-family="Menlo, Consolas, monospace" font-size="18">결과: ORDS 처리 로직이 Header Key를 사용자로 매핑</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1240" height="700" viewBox="0 0 1240 700" role="img" aria-labelledby="title desc">
<title id="title">DDS Bearer Key 처리 검증</title>
<desc id="desc">ORDS Handler가 Bearer Key를 읽어도 DDS EndUserSecurityContext가 붙지 않으면 DATA GRANT가 Key 사용자를 인식하지 못하는 흐름.</desc>
<rect width="1240" height="700" fill="#f8fafc"/>
<defs>
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto">
<path d="M 0 0 L 9 4.5 L 0 9 z" fill="#475569"/>
</marker>
<style>
.h1 { font-family: Arial, Helvetica, sans-serif; font-size: 35px; font-weight: 700; fill: #0f172a; }
.sub { font-family: Arial, Helvetica, sans-serif; font-size: 20px; fill: #475569; }
.title { font-family: Arial, Helvetica, sans-serif; font-size: 21px; font-weight: 700; fill: #0f172a; }
.text { font-family: Arial, Helvetica, sans-serif; font-size: 17px; fill: #334155; }
.mono { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #0f172a; }
.mono-small { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #0f172a; }
.card { fill: #ffffff; stroke: #cbd5e1; stroke-width: 2.2; }
.ok { fill: #ecfdf5; stroke: #059669; stroke-width: 2.2; }
.warn { fill: #fff7ed; stroke: #ea580c; stroke-width: 2.2; }
.bad { fill: #fef2f2; stroke: #dc2626; stroke-width: 2.2; }
.driver { fill: #eff6ff; stroke: #2563eb; stroke-width: 2.2; }
.arrow { stroke: #475569; stroke-width: 2.2; marker-end: url(#arrow); fill: none; }
.dash { stroke: #94a3b8; stroke-width: 2.2; stroke-dasharray: 8 7; marker-end: url(#arrow); fill: none; }
</style>
</defs>
<text x="42" y="56" class="h1">DDS Bearer Key 검증 결과</text>
<text x="42" y="90" class="sub">ORDS Handler의 Header 매핑과 DDS EndUserSecurityContext 전파는 별도 단계</text>
<rect x="42" y="126" width="320" height="122" rx="10" class="card"/>
<text x="68" y="166" class="title">1. Agent 요청</text>
<text x="68" y="204" class="mono">Authorization:</text>
<text x="68" y="230" class="mono">Bearer cb_hr_key</text>
<line x1="362" y1="187" x2="432" y2="187" class="arrow"/>
<rect x="448" y="126" width="344" height="122" rx="10" class="ok"/>
<text x="474" y="166" class="title">2. ORDS Handler</text>
<text x="474" y="204" class="text">Header 필수값 확인</text>
<text x="474" y="230" class="text">Key를 내부 사용자로 매핑</text>
<line x1="792" y1="187" x2="862" y2="187" class="arrow"/>
<rect x="878" y="126" width="320" height="122" rx="10" class="warn"/>
<text x="904" y="166" class="title">3. DB Session</text>
<text x="904" y="204" class="mono">SESSION_USER = CB_ORDS</text>
<text x="904" y="230" class="mono">DDS username = null</text>
<path d="M 1038 248 C 1038 302, 620 302, 620 346" class="arrow"/>
<rect x="448" y="346" width="344" height="142" rx="10" class="bad"/>
<text x="474" y="386" class="title">검증된 차단 결과</text>
<text x="474" y="424" class="mono-small">mapped_end_user=cb_dds_hr</text>
<text x="474" y="452" class="mono">dds_context_username=null</text>
<text x="474" y="480" class="mono">ORA-00942 / EXPECTED_BLOCKED</text>
<path d="M 448 417 C 344 417, 318 312, 250 260" class="dash"/>
<text x="82" y="310" class="text">Key 사용자명을 변수로</text>
<text x="82" y="335" class="text">보관하는 것만으로는</text>
<text x="82" y="360" class="text">DDS 사용자가 되지 않음</text>
<rect x="42" y="540" width="530" height="104" rx="10" class="driver"/>
<text x="68" y="580" class="title">DDS로 Bearer 적용 시 필요한 경로</text>
<text x="68" y="610" class="text">지원 드라이버 또는 호출 앱 계층이</text>
<text x="68" y="634" class="text">DB 호출 전에 EndUserSecurityContext Attach</text>
<line x1="572" y1="592" x2="642" y2="592" class="arrow"/>
<rect x="658" y="540" width="540" height="104" rx="10" class="ok"/>
<text x="684" y="580" class="title">DDS DATA GRANT 적용</text>
<text x="684" y="610" class="text">DB가 End-user Identity와 DATA ROLE 인식</text>
<text x="684" y="634" class="text">보호 객체(VIEW/TABLE) 조회 허용</text>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,88 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="760" viewBox="0 0 1280 760" role="img" aria-labelledby="title desc">
<title id="title">DDS 보안 오브젝트 모델</title>
<desc id="desc">END USER, APPLICATION IDENTITY, DATA ROLE, DATA GRANT, 보호 객체와 중앙 확인 뷰의 관계.</desc>
<rect width="1280" height="760" fill="#f8fafc"/>
<defs>
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M 0 0 L 8 4 L 0 8 z" fill="#475569"/>
</marker>
<style>
.h1 { font-family: Arial, Helvetica, sans-serif; font-size: 34px; font-weight: 700; fill: #0f172a; }
.sub { font-family: Arial, Helvetica, sans-serif; font-size: 19px; fill: #475569; }
.label { font-family: Arial, Helvetica, sans-serif; font-size: 15px; font-weight: 700; fill: #475569; }
.title { font-family: Arial, Helvetica, sans-serif; font-size: 22px; font-weight: 700; fill: #0f172a; }
.text { font-family: Arial, Helvetica, sans-serif; font-size: 17px; fill: #334155; }
.mono { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #1e293b; }
.card { fill: #ffffff; stroke: #cbd5e1; stroke-width: 2; }
.identity { fill: #eff6ff; stroke: #2563eb; stroke-width: 2; }
.role { fill: #f5f3ff; stroke: #7c3aed; stroke-width: 2; }
.grant { fill: #ecfdf5; stroke: #059669; stroke-width: 2; }
.object { fill: #fff7ed; stroke: #ea580c; stroke-width: 2; }
.audit { fill: #eef2ff; stroke: #4f46e5; stroke-width: 2; }
.deny { fill: #fef2f2; stroke: #dc2626; stroke-width: 2; }
.chip { fill: #ffffff; stroke: #bbf7d0; stroke-width: 1.5; }
.arrow { stroke: #475569; stroke-width: 2.2; marker-end: url(#arrow); fill: none; }
.dash { stroke: #64748b; stroke-width: 2; stroke-dasharray: 7 6; marker-end: url(#arrow); fill: none; }
</style>
</defs>
<text x="48" y="58" class="h1">DDS 보안 오브젝트 모델</text>
<text x="48" y="90" class="sub">권한은 업무 매핑 테이블이 아니라 Oracle 보안 오브젝트와 DATA GRANT로 선언</text>
<text x="48" y="130" class="label">사용자 식별</text>
<rect x="48" y="146" width="270" height="132" rx="8" class="identity"/>
<text x="74" y="184" class="title">END USER</text>
<text x="74" y="222" class="text">스키마를 소유하지 않는</text>
<text x="74" y="248" class="text">DDS 보안 사용자</text>
<rect x="48" y="330" width="270" height="112" rx="8" class="card"/>
<text x="74" y="366" class="title">APPLICATION IDENTITY</text>
<text x="74" y="402" class="text">애플리케이션 자체 권한이</text>
<text x="74" y="426" class="text">필요할 때 쓰는 확장</text>
<text x="390" y="130" class="label">역할 묶음</text>
<rect x="390" y="146" width="270" height="132" rx="8" class="role"/>
<text x="416" y="184" class="title">DATA ROLE</text>
<text x="416" y="222" class="text">데이터 권한 묶음</text>
<text x="416" y="248" class="mono">CB_DDS_HR_ROLE</text>
<text x="730" y="130" class="label">권한 선언</text>
<rect x="730" y="118" width="420" height="244" rx="8" class="grant"/>
<text x="760" y="158" class="title">DATA GRANT</text>
<text x="760" y="194" class="text">보호 객체에 대한 작업, 행, 컬럼 범위</text>
<rect x="760" y="218" width="358" height="36" rx="6" class="chip"/>
<text x="780" y="242" class="mono">AS SELECT</text>
<rect x="760" y="264" width="358" height="36" rx="6" class="chip"/>
<text x="780" y="288" class="mono">WHERE dept_code = 'HR'</text>
<rect x="760" y="310" width="358" height="36" rx="6" class="chip"/>
<text x="780" y="334" class="mono">ALL COLUMNS EXCEPT contents</text>
<text x="730" y="410" class="label">보호 대상</text>
<rect x="730" y="426" width="420" height="114" rx="8" class="object"/>
<text x="760" y="466" class="title">VIEW / TABLE</text>
<text x="760" y="502" class="mono">ADMIN.CB_DDS_V_SEARCH_DOCUMENTS</text>
<text x="760" y="526" class="text">DATA GRANT가 있는 범위만 조회 가능</text>
<line x1="318" y1="212" x2="382" y2="212" class="arrow"/>
<text x="326" y="197" class="label">GRANT</text>
<path d="M 318 386 C 350 386, 356 246, 382 230" class="dash"/>
<text x="330" y="352" class="label">선택 확장</text>
<line x1="660" y1="212" x2="722" y2="212" class="arrow"/>
<text x="684" y="197" class="label">TO</text>
<path d="M 940 362 L 940 418" class="arrow"/>
<text x="954" y="398" class="label">ON</text>
<rect x="48" y="574" width="540" height="104" rx="8" class="deny"/>
<text x="74" y="614" class="title">DATA GRANT 없음</text>
<text x="74" y="650" class="text">권한 미부여 END USER에게는 객체 자체가 보이지 않음</text>
<text x="74" y="674" class="mono">ORA-00942</text>
<rect x="640" y="574" width="510" height="104" rx="8" class="audit"/>
<text x="666" y="614" class="title">중앙 확인</text>
<text x="666" y="650" class="mono">DBA_DATA_ROLE_GRANTS</text>
<text x="666" y="674" class="mono">DBA_DATA_GRANTS / DBA_DATA_ROLES</text>
<rect x="48" y="704" width="1102" height="42" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="74" y="731" class="text">요점: DDS는 사용자별 권한을 DATA ROLE과 DATA GRANT로 선언하고, Dictionary View로 적용 상태를 확인한다.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="620" viewBox="0 0 1120 620" role="img" aria-labelledby="title desc">
<title id="title">DDS 결과 화면</title>
<desc id="desc">DDS 권한이 없을 때 조회 대상이 보이지 않는 흐름을 요약한 화면.</desc>
<rect width="1120" height="620" fill="#f4f6f8"/>
<rect x="34" y="30" width="1052" height="560" rx="10" fill="#0f172a"/>
<circle cx="68" cy="58" r="7" fill="#ef4444"/>
<circle cx="92" cy="58" r="7" fill="#f59e0b"/>
<circle cx="116" cy="58" r="7" fill="#22c55e"/>
<text x="150" y="64" fill="#d1d5db" font-family="Menlo, Consolas, monospace" font-size="18">sqlplus - DDS DATA GRANT 테스트</text>
<text x="58" y="112" fill="#a7f3d0" font-family="Menlo, Consolas, monospace" font-size="20">=== DDS 사용자 확인 ===</text>
<text x="58" y="154" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">END_USER_NAME</text>
<text x="58" y="188" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">"cb_dds_hr"</text>
<text x="58" y="246" fill="#a7f3d0" font-family="Menlo, Consolas, monospace" font-size="20">=== DDS 보호 객체 조회 결과 ===</text>
<text x="58" y="288" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">OBJECT_NAME ROWS_VISIBLE CONTENTS</text>
<text x="58" y="322" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">CB_DDS_V_SEARCH_DOCUMENTS 3 NULL</text>
<text x="58" y="356" fill="#fca5a5" font-family="Menlo, Consolas, monospace" font-size="18">cb_dds_none same object ORA-00942: 조회 대상 없음</text>
<text x="58" y="414" fill="#a7f3d0" font-family="Menlo, Consolas, monospace" font-size="20">=== 우회 시도 결과 ===</text>
<text x="58" y="456" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">ADMIN.CB_DDS_DOCUMENTS 직접 조회 - ORA-00942</text>
<text x="58" y="490" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">ADMIN.CB_SEARCH_DOCUMENTS 직접 조회 - ORA-00942</text>
<text x="58" y="524" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">ORDS Bearer 단독 DDS 검증 - EXPECTED_BLOCKED</text>
<text x="58" y="558" fill="#86efac" font-family="Menlo, Consolas, monospace" font-size="18">결과: DATA GRANT가 없으면 행 0건이 아니라 객체 자체가 숨겨짐</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="780" viewBox="0 0 1280 780" role="img" aria-labelledby="title desc">
<title id="title">DDS 설정과 확인 흐름</title>
<desc id="desc">DDS 보호 객체 준비, END USER와 DATA ROLE 생성, DATA GRANT 선언, 중앙 권한 인벤토리 확인 흐름.</desc>
<rect width="1280" height="780" fill="#f8fafc"/>
<defs>
<marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M 0 0 L 8 4 L 0 8 z" fill="#475569"/>
</marker>
<style>
.h1 { font-family: Arial, Helvetica, sans-serif; font-size: 34px; font-weight: 700; fill: #0f172a; }
.sub { font-family: Arial, Helvetica, sans-serif; font-size: 19px; fill: #475569; }
.title { font-family: Arial, Helvetica, sans-serif; font-size: 21px; font-weight: 700; fill: #0f172a; }
.text { font-family: Arial, Helvetica, sans-serif; font-size: 17px; fill: #334155; }
.mono { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #1e293b; }
.head { font-family: Arial, Helvetica, sans-serif; font-size: 15px; font-weight: 700; fill: #334155; }
.cell { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #1e293b; }
.card { fill: #ffffff; stroke: #cbd5e1; stroke-width: 2; }
.blue { fill: #eff6ff; stroke: #2563eb; stroke-width: 2; }
.purple { fill: #f5f3ff; stroke: #7c3aed; stroke-width: 2; }
.green { fill: #ecfdf5; stroke: #059669; stroke-width: 2; }
.orange { fill: #fff7ed; stroke: #ea580c; stroke-width: 2; }
.audit { fill: #eef2ff; stroke: #4f46e5; stroke-width: 2; }
.arrow { stroke: #475569; stroke-width: 2.2; marker-end: url(#arrow); fill: none; }
.rule { stroke: #cbd5e1; stroke-width: 1.5; }
</style>
</defs>
<text x="48" y="58" class="h1">DDS 설정과 확인 흐름</text>
<text x="48" y="90" class="sub">적용은 DATA GRANT 단위로 선언하고, 확인은 Dictionary View에서 통합 조회</text>
<rect x="48" y="128" width="510" height="100" rx="8" class="blue"/>
<text x="74" y="164" class="title">1. 보호 객체 준비</text>
<text x="74" y="198" class="mono">CREATE VIEW / TABLE admin.cb_dds_v_search_documents</text>
<line x1="303" y1="228" x2="303" y2="252" class="arrow"/>
<rect x="48" y="260" width="510" height="112" rx="8" class="purple"/>
<text x="74" y="296" class="title">2. 사용자와 역할 생성</text>
<text x="74" y="330" class="mono">CREATE END USER "cb_dds_hr"</text>
<text x="74" y="356" class="mono">CREATE DATA ROLE cb_dds_hr_role</text>
<line x1="303" y1="372" x2="303" y2="396" class="arrow"/>
<rect x="48" y="404" width="510" height="178" rx="8" class="green"/>
<text x="74" y="440" class="title">3. DATA GRANT 선언</text>
<text x="74" y="474" class="mono">CREATE DATA GRANT admin.cb_dg_hr_docs</text>
<text x="74" y="500" class="mono">AS SELECT (ALL COLUMNS EXCEPT contents)</text>
<text x="74" y="526" class="mono">ON admin.cb_dds_v_search_documents</text>
<text x="74" y="552" class="mono">WHERE dept_code = 'HR' TO cb_dds_hr_role</text>
<line x1="303" y1="582" x2="303" y2="606" class="arrow"/>
<rect x="48" y="614" width="510" height="96" rx="8" class="orange"/>
<text x="74" y="650" class="title">4. 조회 시 자동 적용</text>
<text x="74" y="684" class="text">허용 행과 컬럼만 반환. 권한 미부여 시 ORA-00942.</text>
<rect x="638" y="128" width="582" height="582" rx="8" class="audit"/>
<text x="670" y="166" class="title">5. 권한 인벤토리 확인</text>
<text x="670" y="198" class="text">적용 상태는 아래 Dictionary View를 조합해 확인</text>
<rect x="670" y="224" width="250" height="64" rx="6" class="card"/>
<text x="692" y="250" class="mono">DBA_DATA_GRANTS</text>
<text x="692" y="274" class="text">객체, 조건, 컬럼 범위</text>
<rect x="948" y="224" width="250" height="64" rx="6" class="card"/>
<text x="970" y="250" class="mono">DBA_DATA_ROLE_GRANTS</text>
<text x="970" y="274" class="text">END USER와 역할 매핑</text>
<text x="670" y="332" class="head">END_USER</text>
<text x="832" y="332" class="head">DATA_ROLE</text>
<text x="1012" y="332" class="head">ROW / COLUMN</text>
<line x1="670" y1="346" x2="1198" y2="346" class="rule"/>
<text x="670" y="382" class="cell">cb_dds_hr</text>
<text x="832" y="382" class="cell">HR_ROLE</text>
<text x="1012" y="382" class="cell">HR / EXCEPT contents</text>
<line x1="670" y1="402" x2="1198" y2="402" class="rule"/>
<text x="670" y="438" class="cell">cb_dds_fin</text>
<text x="832" y="438" class="cell">FIN_ROLE</text>
<text x="1012" y="438" class="cell">FIN / EXCEPT contents</text>
<line x1="670" y1="458" x2="1198" y2="458" class="rule"/>
<text x="670" y="494" class="cell">cb_dds_all</text>
<text x="832" y="494" class="cell">ALL_ROLE</text>
<text x="1012" y="494" class="cell">ALL / ALL COLUMNS</text>
<line x1="670" y1="514" x2="1198" y2="514" class="rule"/>
<text x="670" y="550" class="cell">cb_dds_none</text>
<text x="832" y="550" class="cell">CONNECT_ONLY</text>
<text x="1012" y="550" class="cell">- / ORA-00942</text>
<rect x="670" y="610" width="528" height="72" rx="8" fill="#ffffff" stroke="#c7d2fe" stroke-width="2"/>
<text x="694" y="638" class="title">운영 포인트</text>
<text x="694" y="666" class="text">승인 사유와 ticket 번호는 별도 이력 테이블에 보관</text>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,85 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">ORDS Agent 데이터 접근 통제 아키텍처</title>
<desc id="desc">Agent 요청에서 ORDS 사용자 식별, DB 보안 정책 적용, 허용 결과 반환까지의 아키텍처와 DB User, Bearer Key, VPD, DDS 적용 경로.</desc>
<rect width="1280" height="720" fill="#f8fafc"/>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#334155"/>
</marker>
<style>
.h1 { font-family: Arial, Helvetica, sans-serif; font-size: 36px; font-weight: 700; fill: #0f172a; }
.sub { font-family: Arial, Helvetica, sans-serif; font-size: 20px; fill: #475569; }
.title { font-family: Arial, Helvetica, sans-serif; font-size: 21px; font-weight: 700; fill: #0f172a; }
.text { font-family: Arial, Helvetica, sans-serif; font-size: 17px; fill: #334155; }
.small { font-family: Arial, Helvetica, sans-serif; font-size: 15px; fill: #475569; }
.mono { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #1e293b; }
.trunk { fill: #ffffff; stroke: #2563eb; stroke-width: 2; rx: 10; }
.branch { fill: #fefce8; stroke: #ca8a04; stroke-width: 2; rx: 10; }
.policy { fill: #ecfdf5; stroke: #059669; stroke-width: 2; rx: 10; }
.result { fill: #eef2ff; stroke: #4f46e5; stroke-width: 2; rx: 10; }
</style>
</defs>
<text x="54" y="62" class="h1">Agent 권한 매핑 및 데이터 접근 통제</text>
<text x="54" y="100" class="sub">사용자 식별 방식은 갈라지지만, 최종 데이터 제한은 보호 객체(VIEW/TABLE)에서 DB가 적용한다.</text>
<rect x="42" y="154" width="150" height="96" class="trunk"/>
<text x="66" y="192" class="title">1. Agent</text>
<text x="66" y="222" class="text">검색 요청</text>
<line x1="192" y1="202" x2="250" y2="202" stroke="#334155" stroke-width="3" marker-end="url(#arrow)"/>
<rect x="252" y="154" width="160" height="96" class="trunk"/>
<text x="276" y="192" class="title">2. ORDS</text>
<text x="276" y="222" class="text">요청 수신</text>
<line x1="412" y1="202" x2="470" y2="202" stroke="#334155" stroke-width="3" marker-end="url(#arrow)"/>
<rect x="472" y="154" width="190" height="96" class="trunk"/>
<text x="496" y="188" class="title">3. 사용자 식별</text>
<text x="496" y="218" class="text">DB User 또는 Key</text>
<line x1="662" y1="202" x2="720" y2="202" stroke="#334155" stroke-width="3" marker-end="url(#arrow)"/>
<rect x="722" y="154" width="210" height="96" class="trunk"/>
<text x="746" y="188" class="title">4. 권한 기준 생성</text>
<text x="746" y="218" class="text">Context / DDS Identity</text>
<line x1="932" y1="202" x2="990" y2="202" stroke="#334155" stroke-width="3" marker-end="url(#arrow)"/>
<rect x="992" y="154" width="230" height="96" class="trunk"/>
<text x="1016" y="188" class="title">5. 보호 객체</text>
<text x="1016" y="216" class="text">VIEW/TABLE 조회</text>
<text x="1016" y="239" class="text">정책 적용</text>
<rect x="446" y="318" width="245" height="126" class="branch"/>
<text x="474" y="356" class="title">식별 경로 A</text>
<text x="474" y="386" class="mono">SESSION_USER</text>
<text x="474" y="416" class="text">DB 계정으로 사용자 매핑</text>
<line x1="568" y1="318" x2="568" y2="250" stroke="#ca8a04" stroke-width="2.5" marker-end="url(#arrow)"/>
<rect x="156" y="318" width="245" height="126" class="branch"/>
<text x="184" y="356" class="title">식별 경로 B</text>
<text x="184" y="386" class="mono">Authorization: Bearer</text>
<text x="184" y="416" class="text">Key로 내부 사용자 매핑</text>
<line x1="400" y1="372" x2="472" y2="235" stroke="#ca8a04" stroke-width="2.5" marker-end="url(#arrow)"/>
<rect x="736" y="318" width="245" height="126" class="branch"/>
<text x="764" y="356" class="title">식별 경로 C</text>
<text x="764" y="386" class="mono">END USER</text>
<text x="764" y="416" class="text">DDS 보안 사용자</text>
<line x1="828" y1="318" x2="828" y2="250" stroke="#ca8a04" stroke-width="2.5" marker-end="url(#arrow)"/>
<rect x="230" y="508" width="340" height="126" class="policy"/>
<text x="260" y="548" class="title">VPD 적용 경로</text>
<text x="260" y="578" class="text">SYS_CONTEXT + p_object + EXISTS</text>
<text x="260" y="606" class="small">권한 테이블 기반 행 제한</text>
<text x="260" y="626" class="small">Redaction 값 마스킹</text>
<line x1="570" y1="562" x2="746" y2="250" stroke="#059669" stroke-width="2.5" marker-end="url(#arrow)"/>
<rect x="710" y="508" width="340" height="126" class="policy"/>
<text x="740" y="548" class="title">DDS 적용 경로</text>
<text x="740" y="578" class="text">DATA ROLE + DATA GRANT</text>
<text x="740" y="606" class="small">행 / 컬럼 / 작업 통제</text>
<text x="740" y="626" class="small">선언형 제한</text>
<line x1="880" y1="508" x2="1048" y2="250" stroke="#059669" stroke-width="2.5" marker-end="url(#arrow)"/>
<rect x="368" y="654" width="544" height="48" class="result"/>
<text x="410" y="685" class="title">결과: 허용된 행과 컬럼만 반환</text>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="760" viewBox="0 0 1280 760" role="img" aria-labelledby="title desc">
<title id="title">권한 미부여 및 차단 결과 화면</title>
<desc id="desc">권한 미부여 시 VPD 0건, DB 권한 오류, Bearer Key 오류가 어떻게 다른지 보여주는 화면.</desc>
<rect width="1280" height="760" fill="#f4f6f8"/>
<rect x="36" y="30" width="1208" height="700" rx="10" fill="#172033"/>
<circle cx="70" cy="58" r="7" fill="#ef4444"/>
<circle cx="94" cy="58" r="7" fill="#f59e0b"/>
<circle cx="118" cy="58" r="7" fill="#22c55e"/>
<text x="152" y="64" fill="#d1d5db" font-family="Menlo, Consolas, monospace" font-size="18">권한 미부여 및 차단 결과 비교</text>
<text x="60" y="116" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== Case A. DB SELECT Grant 있음 + VPD Policy 있음 + 매핑 없음 ===</text>
<text x="60" y="158" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">SELECT COUNT(*) FROM ADMIN.CB_V_SEARCH_DOCUMENTS;</text>
<text x="60" y="196" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="17">ROWS_VISIBLE</text>
<text x="60" y="226" fill="#86efac" font-family="Menlo, Consolas, monospace" font-size="17">0</text>
<text x="232" y="226" fill="#c7d2fe" font-family="Menlo, Consolas, monospace" font-size="17">해석: SQL은 실행되지만 VPD EXISTS가 통과하지 못함</text>
<text x="60" y="294" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== Case B. 보호 객체 DB 권한 미부여 ===</text>
<text x="60" y="336" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">SELECT COUNT(*) FROM ADMIN.CB_V_SEARCH_DOCUMENTS;</text>
<text x="60" y="374" fill="#fca5a5" font-family="Menlo, Consolas, monospace" font-size="17">ORA-00942: table or view does not exist</text>
<text x="472" y="374" fill="#c7d2fe" font-family="Menlo, Consolas, monospace" font-size="17">해석: VPD 판단 전 객체가 보이지 않음</text>
<text x="60" y="442" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== Case C. Bearer Key 누락 또는 형식 오류 ===</text>
<text x="60" y="484" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">Authorization Header 없음</text>
<text x="60" y="522" fill="#fca5a5" font-family="Menlo, Consolas, monospace" font-size="17">ORA-20101: Authorization header must be Bearer &lt;key&gt;</text>
<text x="628" y="522" fill="#c7d2fe" font-family="Menlo, Consolas, monospace" font-size="17">해석: ORDS Handler 필수값 차단</text>
<text x="60" y="590" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== Case D. Bearer Key가 틀리거나 만료됨 ===</text>
<text x="60" y="632" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">Authorization: Bearer invalid_key</text>
<text x="60" y="670" fill="#fca5a5" font-family="Menlo, Consolas, monospace" font-size="17">ORA-20002: Invalid or expired Bearer key</text>
<text x="472" y="670" fill="#c7d2fe" font-family="Menlo, Consolas, monospace" font-size="17">해석: Key Hash 매핑 실패, Context 초기화</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="760" viewBox="0 0 1280 760" role="img" aria-labelledby="title desc">
<title id="title">권한 등록 매핑 화면</title>
<desc id="desc">Bearer Key 사용자를 Role, Permission, 행 규칙으로 등록하는 운영 화면.</desc>
<rect width="1280" height="760" fill="#f4f6f8"/>
<rect x="36" y="30" width="1208" height="700" rx="10" fill="#111827"/>
<circle cx="70" cy="58" r="7" fill="#ef4444"/>
<circle cx="94" cy="58" r="7" fill="#f59e0b"/>
<circle cx="118" cy="58" r="7" fill="#22c55e"/>
<text x="152" y="64" fill="#d1d5db" font-family="Menlo, Consolas, monospace" font-size="18">ADMIN - Agent 권한 등록 매핑</text>
<text x="60" y="112" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== 1. 내부 사용자와 Bearer Key 등록 ===</text>
<text x="60" y="154" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">APP_USER: USER_ID=101, USER_NAME=agent_hr, EMP_NO=E10234, DEPT_CODE=HR, READ_CONTENTS=N</text>
<text x="60" y="188" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">AGENT_BEARER_KEY: KEY_ID=1, USER_ID=101, KEY_HASH=SHA256(cb_hr_key), ACTIVE=Y</text>
<text x="60" y="244" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== 2. 사용자 -> 역할 -> 보호 객체 권한 ===</text>
<text x="60" y="286" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">USER_ROLE: USER_ID=101 -> ROLE_ID=10</text>
<text x="60" y="320" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">APP_ROLE: ROLE_ID=10 -> HR_SEARCH_ROLE</text>
<text x="60" y="354" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">PERMISSION: ROLE_ID=10 -> TARGET_NAME=CB_V_SEARCH_DOCUMENTS, ACTION=SELECT</text>
<text x="60" y="410" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== 3. 행 / 컬럼 제한 ===</text>
<text x="60" y="452" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">PERMISSION_RULE: PERM_ID=100, RULE_TYPE=MY_DEPT, RULE_VALUE=HR</text>
<text x="60" y="486" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="17">COLUMN RULE: APP_USER.CAN_READ_CONTENTS=N -> DBMS_REDACT가 CONTENTS를 NULL 처리</text>
<text x="60" y="542" fill="#bfdbfe" font-family="Menlo, Consolas, monospace" font-size="20">=== 4. 조회 시 VPD 매칭 ===</text>
<text x="60" y="584" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="17">p_object=CB_V_SEARCH_DOCUMENTS</text>
<text x="60" y="618" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="17">permission.target_name=CB_V_SEARCH_DOCUMENTS</text>
<text x="60" y="652" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="17">rule_type=MY_DEPT + SYS_CONTEXT(DEPT_CODE)=HR -> HR 행만 반환</text>
<text x="60" y="696" fill="#86efac" font-family="Menlo, Consolas, monospace" font-size="18">결과: Key User의 role/permission/rule이 있어야 행이 반환됨</text>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="700" viewBox="0 0 1280 700" role="img" aria-labelledby="title desc">
<title id="title">SYS_CONTEXT 동작 방식</title>
<desc id="desc">ORDS 처리 로직이 DB 세션에 값을 저장하고 VPD 함수가 SYS_CONTEXT로 값을 읽어 EXISTS 권한 조회에 사용하는 흐름.</desc>
<rect width="1280" height="700" fill="#f8fafc"/>
<defs>
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto">
<path d="M 0 0 L 9 4.5 L 0 9 z" fill="#475569"/>
</marker>
<style>
.h1 { font-family: Arial, Helvetica, sans-serif; font-size: 39px; font-weight: 700; fill: #0f172a; }
.sub { font-family: Arial, Helvetica, sans-serif; font-size: 21px; fill: #475569; }
.card { fill: #ffffff; stroke: #cbd5e1; stroke-width: 2; }
.blue { fill: #eff6ff; stroke: #2563eb; stroke-width: 2; }
.purple { fill: #f5f3ff; stroke: #7c3aed; stroke-width: 2; }
.green { fill: #ecfdf5; stroke: #059669; stroke-width: 2; }
.warn { fill: #fff7ed; stroke: #ea580c; stroke-width: 2; }
.title { font-family: Arial, Helvetica, sans-serif; font-size: 24px; font-weight: 700; fill: #0f172a; }
.text { font-family: Arial, Helvetica, sans-serif; font-size: 18px; fill: #334155; }
.mono { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #0f172a; }
.arrow { stroke: #475569; stroke-width: 2.5; marker-end: url(#arrow); fill: none; }
</style>
</defs>
<text x="48" y="58" class="h1">SYS_CONTEXT 동작 방식</text>
<text x="48" y="94" class="sub">현재 DB 세션에 저장된 요청자 값을 읽어 권한 테이블 EXISTS 조회에 사용</text>
<rect x="48" y="142" width="320" height="152" rx="16" class="blue"/>
<text x="76" y="184" class="title">1. ORDS 처리 로직</text>
<text x="76" y="224" class="text">DB 계정 또는 Bearer Key로</text>
<text x="76" y="252" class="text">요청자를 식별</text>
<line x1="368" y1="218" x2="438" y2="218" class="arrow"/>
<rect x="450" y="142" width="360" height="152" rx="16" class="purple"/>
<text x="478" y="184" class="title">2. DB 세션에 값 저장</text>
<text x="478" y="224" class="mono">DBMS_SESSION.SET_CONTEXT</text>
<text x="478" y="252" class="mono">AGENT_CTX.EMP_NO = E10234</text>
<text x="478" y="278" class="mono">AGENT_CTX.DEPT_CODE = HR</text>
<line x1="810" y1="218" x2="850" y2="218" class="arrow"/>
<rect x="862" y="142" width="370" height="152" rx="10" class="green"/>
<text x="890" y="184" class="title">3. EXISTS 조회에 사용</text>
<text x="890" y="224" class="mono">SYS_CONTEXT('AGENT_CTX','EMP_NO')</text>
<text x="890" y="252" class="mono">SYS_CONTEXT('AGENT_CTX','DEPT_CODE')</text>
<rect x="118" y="380" width="500" height="126" rx="10" class="card"/>
<text x="150" y="424" class="title">같은 DB 세션 안에서만 유효</text>
<text x="150" y="462" class="text">요청마다 기존 값을 지우고 새 값을 저장</text>
<text x="150" y="490" class="text">다른 요청자 정보가 섞이지 않도록 처리</text>
<rect x="682" y="380" width="500" height="126" rx="10" class="warn"/>
<text x="714" y="424" class="title">직접 조작 방지</text>
<text x="714" y="462" class="text">CONTEXT는 지정된 패키지를 통해서만 설정</text>
<text x="714" y="490" class="text">일반 사용자가 임의로 값을 바꾸지 못하게 구성</text>
<rect x="220" y="574" width="840" height="54" rx="14" fill="#eef2ff" stroke="#4f46e5" stroke-width="2.5"/>
<text x="250" y="609" class="title">요점: SET_CONTEXT는 저장, SYS_CONTEXT는 조회, EXISTS가 권한 판단</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,105 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800" role="img" aria-labelledby="title desc">
<title id="title">두 가지 사용자 식별 및 권한 매핑 시나리오</title>
<desc id="desc">DB User 기반과 Bearer Key 기반의 사용자 식별 차이와 공통 DB 보안 적용 흐름.</desc>
<rect width="1280" height="800" fill="#f8fafc"/>
<defs>
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto">
<path d="M 0 0 L 9 4.5 L 0 9 z" fill="#475569"/>
</marker>
<style>
.h1 { font-family: Arial, Helvetica, sans-serif; font-size: 36px; font-weight: 700; fill: #0f172a; }
.sub { font-family: Arial, Helvetica, sans-serif; font-size: 20px; fill: #475569; }
.panel { fill: #ffffff; stroke: #cbd5e1; stroke-width: 2; }
.panel-a { fill: #f8fbff; stroke: #2563eb; stroke-width: 2; }
.panel-b { fill: #fffaf5; stroke: #ea580c; stroke-width: 2; }
.shared { fill: #f7f5ff; stroke: #7c3aed; stroke-width: 2; }
.result { fill: #ecfdf5; stroke: #059669; stroke-width: 2; }
.title { font-family: Arial, Helvetica, sans-serif; font-size: 24px; font-weight: 700; fill: #0f172a; }
.label { font-family: Arial, Helvetica, sans-serif; font-size: 18px; font-weight: 700; fill: #0f172a; }
.text { font-family: Arial, Helvetica, sans-serif; font-size: 17px; fill: #334155; }
.small { font-family: Arial, Helvetica, sans-serif; font-size: 16px; fill: #475569; }
.mono { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #1e293b; }
.warn { font-family: Arial, Helvetica, sans-serif; font-size: 16px; font-weight: 700; fill: #b91c1c; }
.arrow { stroke: #475569; stroke-width: 2.2; marker-end: url(#arrow); fill: none; }
</style>
</defs>
<text x="48" y="58" class="h1">두 가지 사용자 식별 시나리오</text>
<text x="48" y="94" class="sub">사용자 식별 방식은 다르고, 권한 적용은 DB 보안 정책에서 동일하게 수행</text>
<rect x="48" y="128" width="572" height="292" rx="10" class="panel-a"/>
<text x="76" y="176" class="title">시나리오 1 - DB User 기반</text>
<rect x="76" y="204" width="156" height="118" rx="10" class="panel"/>
<text x="96" y="240" class="label">DB 접속 계정</text>
<text x="96" y="276" class="mono">SESSION_USER</text>
<text x="96" y="304" class="mono">AGENT_HR_001</text>
<line x1="232" y1="263" x2="270" y2="263" class="arrow"/>
<rect x="276" y="204" width="164" height="118" rx="10" class="panel"/>
<text x="296" y="240" class="label">사용자 매핑</text>
<text x="296" y="276" class="mono">app_user</text>
<text x="296" y="304" class="mono">user_role</text>
<line x1="440" y1="263" x2="478" y2="263" class="arrow"/>
<rect x="484" y="204" width="88" height="118" rx="10" class="panel"/>
<text x="506" y="240" class="label">식별</text>
<text x="504" y="276" class="mono">사번</text>
<text x="504" y="304" class="mono">부서</text>
<rect x="76" y="346" width="496" height="42" rx="8" fill="#eff6ff" stroke="#bfdbfe" stroke-width="2"/>
<text x="96" y="374" class="small">DB 계정별 권한 등록. DBA가 사용자-역할-권한 테이블 관리</text>
<rect x="660" y="128" width="572" height="292" rx="10" class="panel-b"/>
<text x="688" y="176" class="title">시나리오 2 - Bearer Key 기반</text>
<rect x="688" y="204" width="156" height="118" rx="10" class="panel"/>
<text x="708" y="240" class="label">ORDS Header</text>
<text x="708" y="276" class="mono">Bearer Key</text>
<text x="708" y="304" class="warn">없으면 차단</text>
<line x1="844" y1="263" x2="882" y2="263" class="arrow"/>
<rect x="888" y="204" width="164" height="118" rx="10" class="panel"/>
<text x="908" y="240" class="label">Key 검증</text>
<text x="908" y="276" class="mono">key_hash</text>
<text x="908" y="304" class="mono">agent_key</text>
<line x1="1052" y1="263" x2="1090" y2="263" class="arrow"/>
<rect x="1096" y="204" width="88" height="118" rx="10" class="panel"/>
<text x="1118" y="240" class="label">식별</text>
<text x="1116" y="276" class="mono">사번</text>
<text x="1116" y="304" class="mono">부서</text>
<rect x="688" y="346" width="496" height="42" rx="8" fill="#fff7ed" stroke="#fed7aa" stroke-width="2"/>
<text x="708" y="374" class="small">ORDS 처리 로직이 Header 값을 받아 내부 사용자로 매핑</text>
<path d="M 334 420 C 334 452, 438 468, 520 492" class="arrow"/>
<path d="M 946 420 C 946 452, 842 468, 760 492" class="arrow"/>
<rect x="178" y="494" width="430" height="110" rx="10" class="shared"/>
<text x="214" y="538" class="title">현재 요청 사용자 정보</text>
<text x="214" y="574" class="text">USER_ID / 사번 / 부서 저장</text>
<rect x="672" y="494" width="450" height="110" rx="10" class="shared"/>
<text x="708" y="538" class="title">보호 객체 조회</text>
<text x="708" y="574" class="text">VIEW 또는 TABLE에 연결된 정책 적용</text>
<line x1="640" y1="604" x2="640" y2="636" class="arrow"/>
<rect x="178" y="638" width="450" height="76" rx="14" class="shared"/>
<text x="212" y="684" class="label">VPD: WHERE 조건 자동 추가</text>
<rect x="672" y="638" width="450" height="76" rx="14" class="shared"/>
<text x="706" y="684" class="label">DDS: DATA GRANT WHERE 적용</text>
<line x1="628" y1="676" x2="672" y2="676" class="arrow"/>
<line x1="640" y1="714" x2="640" y2="732" class="arrow"/>
<rect x="420" y="730" width="440" height="48" rx="12" class="result"/>
<text x="512" y="762" class="label">결과: 권한 범위 데이터만 반환</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="620" viewBox="0 0 1120 620" role="img" aria-labelledby="title desc">
<title id="title">VPD 결과 화면</title>
<desc id="desc">VPD 행 필터링과 우회 시도 차단 결과를 요약한 화면.</desc>
<rect width="1120" height="620" fill="#f4f6f8"/>
<rect x="34" y="30" width="1052" height="560" rx="10" fill="#111827"/>
<circle cx="68" cy="58" r="7" fill="#ef4444"/>
<circle cx="92" cy="58" r="7" fill="#f59e0b"/>
<circle cx="116" cy="58" r="7" fill="#22c55e"/>
<text x="150" y="64" fill="#d1d5db" font-family="Menlo, Consolas, monospace" font-size="18">sqlplus - VPD 보안 정책 테스트</text>
<text x="58" y="112" fill="#93c5fd" font-family="Menlo, Consolas, monospace" font-size="20">=== DB 세션과 Key User Context ===</text>
<text x="58" y="154" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">DB_USER KEY_USER_ID EMP_NO DEPT_CODE READ_CONTENTS</text>
<text x="58" y="188" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">CB_ORDS 101 E10234 HR N</text>
<text x="58" y="246" fill="#93c5fd" font-family="Menlo, Consolas, monospace" font-size="20">=== CB_V_SEARCH_DOCUMENTS 조회 결과 ===</text>
<text x="58" y="288" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">KEY ROWS_VISIBLE CONTENTS</text>
<text x="58" y="322" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">cb_hr_key 3 NULL</text>
<text x="58" y="356" fill="#f9fafb" font-family="Menlo, Consolas, monospace" font-size="18">cb_all_key 6 원문 표시</text>
<text x="58" y="414" fill="#93c5fd" font-family="Menlo, Consolas, monospace" font-size="20">=== 우회 시도 결과 ===</text>
<text x="58" y="456" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">read ADMIN.CB_SEARCH_DOCUMENTS - ORA-00942</text>
<text x="58" y="490" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">read ADMIN.CB_AGENT_BEARER_KEY - ORA-00942</text>
<text x="58" y="524" fill="#e5e7eb" font-family="Menlo, Consolas, monospace" font-size="18">Invalid Bearer Key - ORA-20002</text>
<text x="58" y="558" fill="#86efac" font-family="Menlo, Consolas, monospace" font-size="18">결과: Agent가 아니라 DB가 최종 필터링</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,73 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="760" viewBox="0 0 1280 760" role="img" aria-labelledby="title desc">
<title id="title">VPD EXISTS 권한 조건 적용 흐름</title>
<desc id="desc">ORDS 조회 SQL에 VPD가 p_object 기준 EXISTS 권한 조건을 추가해 VIEW/TABLE 접근과 행 접근을 판단하는 흐름.</desc>
<rect width="1280" height="760" fill="#f8fafc"/>
<defs>
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto">
<path d="M 0 0 L 9 4.5 L 0 9 z" fill="#475569"/>
</marker>
<style>
.h1 { font-family: Arial, Helvetica, sans-serif; font-size: 36px; font-weight: 700; fill: #0f172a; }
.sub { font-family: Arial, Helvetica, sans-serif; font-size: 20px; fill: #475569; }
.panel { fill: #ffffff; stroke: #cbd5e1; stroke-width: 2; }
.blue { fill: #eff6ff; stroke: #2563eb; stroke-width: 2; }
.purple { fill: #f5f3ff; stroke: #7c3aed; stroke-width: 2; }
.green { fill: #ecfdf5; stroke: #059669; stroke-width: 2; }
.warn { fill: #fff7ed; stroke: #ea580c; stroke-width: 2; }
.title { font-family: Arial, Helvetica, sans-serif; font-size: 23px; font-weight: 700; fill: #0f172a; }
.text { font-family: Arial, Helvetica, sans-serif; font-size: 18px; fill: #334155; }
.mono { font-family: Menlo, Consolas, monospace; font-size: 17px; fill: #0f172a; }
.mono-small { font-family: Menlo, Consolas, monospace; font-size: 15px; fill: #0f172a; }
.arrow { stroke: #475569; stroke-width: 2.2; marker-end: url(#arrow); fill: none; }
</style>
</defs>
<text x="48" y="58" class="h1">VPD: EXISTS로 권한 테이블 확인</text>
<text x="48" y="94" class="sub">p_object는 현재 조회 대상, SYS_CONTEXT는 요청자 식별값, EXISTS는 실제 권한 판단</text>
<rect x="48" y="138" width="360" height="276" rx="10" class="blue"/>
<text x="76" y="184" class="title">1. ORDS가 실행한 SQL</text>
<text x="76" y="228" class="text">권한 조건 없음</text>
<text x="76" y="278" class="mono">SELECT doc_id, title</text>
<text x="76" y="306" class="mono">FROM app.v_search_documents</text>
<text x="76" y="334" class="mono">WHERE contains_text = :q;</text>
<line x1="408" y1="276" x2="468" y2="276" class="arrow"/>
<rect x="480" y="138" width="360" height="276" rx="10" class="purple"/>
<text x="508" y="184" class="title">2. VPD 정책 함수</text>
<text x="508" y="226" class="text">조회 대상과 요청자 확인</text>
<text x="508" y="264" class="mono">p_object = 현재 VIEW/TABLE</text>
<text x="508" y="292" class="mono">USER_ID = SYS_CONTEXT(...)</text>
<text x="508" y="344" class="mono-small">RETURN EXISTS (...)</text>
<text x="508" y="370" class="mono-small">target_name = p_object</text>
<line x1="840" y1="276" x2="900" y2="276" class="arrow"/>
<rect x="912" y="138" width="360" height="276" rx="10" class="green"/>
<text x="940" y="184" class="title">3. DB가 합쳐서 적용</text>
<text x="940" y="226" class="text">조회 SQL 뒤에 권한 조건 추가</text>
<text x="940" y="276" class="mono-small">WHERE contains_text = :q</text>
<text x="940" y="304" class="mono-small">AND EXISTS (</text>
<text x="940" y="332" class="mono-small"> permission.target = p_object</text>
<text x="940" y="360" class="mono-small"> row rule matched)</text>
<rect x="74" y="486" width="360" height="118" rx="10" class="panel"/>
<text x="106" y="530" class="title">VIEW/TABLE 접근 판단</text>
<text x="106" y="568" class="text">target_name = p_object</text>
<text x="106" y="594" class="text">없으면 결과 0건</text>
<rect x="460" y="486" width="360" height="118" rx="10" class="panel"/>
<text x="492" y="530" class="title">행 접근 판단</text>
<text x="492" y="568" class="text">permission_rule이 행 컬럼과 일치</text>
<text x="492" y="594" class="text">조건에 맞는 행만 반환</text>
<rect x="846" y="486" width="360" height="118" rx="10" class="warn"/>
<text x="878" y="530" class="title">권한 매핑 없음</text>
<text x="878" y="568" class="text">EXISTS가 false</text>
<text x="878" y="594" class="text">해당 행은 제외</text>
<rect x="190" y="650" width="900" height="58" rx="10" fill="#eef2ff" stroke="#4f46e5" stroke-width="2"/>
<text x="236" y="686" class="title">요점: p_object는 조회 대상, SYS_CONTEXT는 요청자, EXISTS가 권한 판단</text>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,5 +1,30 @@
# Redmine #546 - 외부 VM 배포 설계
## 현재 기준 배포 대상
이 문서의 최신 운영 기준은 아래와 같다. 과거 `hermes`/`130.162.134.59` 기록은 초기 개발·실험 배포 이력으로만 본다.
| 구분 | 값 |
| --- | --- |
| 개발·빌드 VM | `hermes` |
| 공개 서비스 배포 VM | `opc@161.33.6.45` (`vnic-aidp-poc`) |
| 공개 주소 | `https://kb.cloud-handson.com` |
| DNS | `kb.cloud-handson.com → 161.33.6.45` |
| VM 서비스 | `vpd-backoffice.service` |
| 앱 수신 주소 | `127.0.0.1:8080` |
| 공개 프록시 | Nginx `80/443 → 127.0.0.1:8080` |
| 인증서 | Lets Encrypt / Certbot Nginx plugin |
| 앱 디렉터리 | `/home/opc/apps/vpd-backoffice` |
| Wallet 디렉터리 | `/home/opc/apps/vpd-backoffice/wallet` |
반복 배포 시 완료 판정은 반드시 공개 주소 기준으로 한다.
```bash
curl -k -sS https://kb.cloud-handson.com/login
```
`hermes` 내부의 `8082` 응답만 확인하고 완료 처리하지 않는다. `8082`는 과거/개발 배포 경로에 해당할 수 있다.
## 프로젝트 개요
VPD Backoffice는 Oracle Database VPD/ORDS 기능을 백오피스 권한 테이블로 제어하는 Spring Boot 관리 도구다. 사용자는 Oracle DB schema user가 아니라 Bearer Token으로 식별되는 application user이며, 사용자/그룹/역할/권한/행 규칙/컬럼 NULL 처리 설정이 VPD policy function과 ORDS 조회 결과에 반영된다.
@@ -16,7 +41,8 @@ VPD Backoffice는 Oracle Database VPD/ORDS 기능을 백오피스 권한 테이
## 배포 방식
- 기본 대상 SSH host는 `hermes`로 두되, 스크립트 인자로 다른 SSH alias를 받을 수 있게 한다.
- 기본 개발/빌드 host는 `hermes`일 수 있으나, 공개 서비스 배포 대상은 `opc@161.33.6.45`다.
- 배포 스크립트의 기본값이 `hermes`인 경우, 운영 배포에서는 반드시 `--host` 또는 SSH alias가 `161.33.6.45`를 가리키는지 확인한다.
- 로컬에서 `mvn -DskipTests package`로 jar를 빌드한다.
- 원격 디렉토리 기본값은 `~/apps/vpd-backoffice`다.
- 배포 패키지 구성:
@@ -39,9 +65,9 @@ VPD Backoffice는 Oracle Database VPD/ORDS 기능을 백오피스 권한 테이
- `scripts/deploy-backoffice-vm.sh`가 jar, `.env`, wallet, start/stop/status 스크립트를 원격에 설치할 수 있다.
- 대상 alias가 없으면 명확하게 실패한다.
- 배포 후 원격 `status.sh`와 HTTP `/login` 헬스체크가 가능하다.
- OCI NSG와 VM firewalld에서 운영자 IP 기준 `8082/tcp` 접근을 허용한다.
- 현재 공개 운영에서는 Nginx 80/443만 외부에 열고, Spring Boot 애플리케이션 포트는 VM 내부 loopback으로 제한한다.
## 배포 결과
## 과거 hermes 배포 결과
- 배포 대상: `hermes` / `opc@130.162.134.59`
- 원격 경로: `/home/opc/apps/vpd-backoffice`
@@ -77,8 +103,8 @@ VPD Backoffice는 Oracle Database VPD/ORDS 기능을 백오피스 권한 테이
- `mvn test`
- `scripts/deploy-backoffice-vm.sh --dry-run`
- `ssh hermes` 접속 확인
- `scripts/deploy-backoffice-vm.sh --host hermes --skip-build`
- 운영 배포 대상 SSH alias가 `opc@161.33.6.45`를 가리키는지 확인
- 운영 배포 대상의 `systemctl status vpd-backoffice` 확인
- 원격 내부 `/login` HTTP 200
- 외부 `/login` HTTP 200
- `systemctl status vpd-backoffice``active (running)`이고 앱 로그에서 `vpd-backoffice-pool` 연결이 성공한다.

View File

@@ -0,0 +1,200 @@
# 설계서: PoC_4 MCP Discovery UI — KB VPD Streamable HTTP 연동 정비 (#620)
> **상태**: Draft
> **작성**: [AI] Architect · **최종수정**: 2026-07-09
> **추적성** — Redmine: #620 · 관련 ADR: 없음
> · 구현 파일: `apps/poc4/mcp_discovery_ui.py`, `config/mcp_servers.json`, `config/mcp_servers.sample.json`, VPD Backoffice의 `/mcp` endpoint · 테스트: PoC_4 단위 테스트 및 실제 MCP HTTP smoke test
## 1. 목적 (Why)
PoC_4의 MCP Discovery UI가 KB VPD MCP를 사용자별 Bearer 토큰으로 안전하게 호출하면서도, 환경별 URL·전송 방식·오류 처리를 하나의 명확한 계약으로 관리한다.
현재 UI의 기본 호출 흐름은 동작한다. 다만 `kb_mcp` 설정에는 `endpoint_url``base_url_env`가 함께 있고, UI는 `endpoint_url`을 우선 사용한다. 따라서 `KB_MCP_BASE_URL`을 바꿔도 실제 호출 대상이 바뀌지 않는다. 또한 `custom_python`은 로컬 8500 MCP용 이름이므로 외부 KB VPD MCP의 통신 계약을 설명하지 못한다.
## 2. 범위 (Scope)
- **포함**:
- `apps/poc4/mcp_discovery_ui.py`의 KB MCP endpoint, 인증 헤더, JSON-RPC, 오류 처리 정비
- `config/mcp_servers.json` 및 sample의 KB MCP 선언 정비
- KB MCP의 단일 도구 `ords.query.kb_select_ai_vpd` 호출 계약 문서화
- VPD Backoffice `/mcp`과의 HTTP 상태·프로토콜 버전 호환성 점검 및 필요한 최소 보완
- **제외 (out of scope)**:
- 기존 `kb_vector_mcp``custom_python` 8500 RAG MCP의 변경
- VPD 권한 규칙, Select AI SQL 생성 규칙, ORDS 비즈니스 로직 변경
- OAuth Authorization Server, 사용자 로그인/토큰 발급 UI 재설계
- VPD 토큰 원문을 환경변수·레지스트리에 저장하는 방식
## 3. 인수조건 (Acceptance Criteria)
- [ ] KB MCP URL은 `KB_MCP_BASE_URL` 하나에서만 해석되고 `/mcp` path가 안전하게 결합된다.
- [ ] `initialize`, `notifications/initialized`, `tools/list`, `tools/call`의 모든 HTTP 요청에 현재 선택된 사용자의 `Authorization: Bearer <VPD token>`만 전송된다.
- [ ] Bearer 토큰은 JSON-RPC arguments, SQLite 대화 이력, UI 결과, 애플리케이션 로그에 저장·출력되지 않는다.
- [ ] 호출 가능한 도구는 `ords.query.kb_select_ai_vpd` 하나이며, 인자는 `prompt``limit`만 허용된다.
- [ ] 단일 도구 구성에서는 LLM router를 호출하지 않고 허용 도구를 직접 선택한다.
- [ ] 토큰 없음/위조/만료는 HTTP `401`, 유효 토큰의 권한 부족은 HTTP `403`으로 UI에 구분 표시된다.
- [ ] 잘못된 Origin의 브라우저 요청은 MCP 서버에서 거부되며, 허용 Origin 및 Origin 없는 네이티브 MCP client 정책이 문서화된다.
- [ ] 기존 `kb_vector_mcp` 및 로컬 8500 MCP 회귀 테스트가 통과한다.
## 4. 컨텍스트 & 제약
- KB VPD MCP endpoint: `https://kb.cloud-handson.com/mcp`
- 보호 대상: `ords.query.kb_select_ai_vpd`는 ORDS를 거쳐 VPD 컨텍스트가 적용된 Select AI 조회를 실행한다.
- 토큰 주체: VPD 권한은 정적 서비스 계정이 아니라 현재 선택된 `KB_STAKEHOLDERS` 사용자 토큰에 의해 결정된다.
- UI의 VPD token preset 파일은 데모 편의 기능일 뿐이다. 운영에서는 OS 소유자 전용 권한(`0600`)으로 관리하고 형상관리·로그·SQLite에서 제외한다.
- UI가 현재 사용하는 `2025-11-25` MCP protocol version과 서버의 지원 버전은 handshake에서 협상해야 한다. 지원하지 않는 버전을 무조건 강제하지 않는다.
- 현재 서버는 stateless JSON-RPC POST 호출로도 동작한다. 서버가 `Mcp-Session-Id`를 발급하면 client는 이후 요청에만 그 값을 포함한다.
## 5. 아키텍처 개요
I/O는 Discovery UI의 HTTP transport와 VPD Backoffice `/mcp`에 한정한다. URL 결합, 허용 도구 검증, 요청·응답 검증, 안전한 오류 변환은 순수 함수로 분리해 네트워크 없이 테스트한다.
```
VPD 사용자 선택 / 토큰 입력
│ (원문은 요청 메모리에만 존재)
PoC_4 MCP Discovery UI
├─ KB_MCP_BASE_URL + "/mcp"
├─ tool allowlist 검증
└─ Authorization: Bearer <current VPD token>
▼ HTTPS JSON-RPC / Streamable HTTP
VPD Backoffice MCP (/mcp)
├─ Origin·토큰 검증
├─ tools/list: metadata only
└─ tools/call: ords.query.kb_select_ai_vpd
ORDS Select AI API → VPD context → Oracle ADB
```
## 6. 데이터 모델
### 6.1 KB MCP registry 선언
`config/mcp_servers.json`의 KB 선언은 다음 계약을 따른다. 값은 예시이며, 토큰 값은 넣지 않는다.
```json
{
"id": "kb_mcp",
"enabled": true,
"provider": "kb_vpd_streamable_http",
"transport": "streamable_http",
"base_url_env": "KB_MCP_BASE_URL",
"endpoint_path": "/mcp",
"auth_delivery": "per_request_vpd_bearer",
"timeout_seconds_env": "POC3_MCP_TIMEOUT_SECONDS",
"default_tool": "ords.query.kb_select_ai_vpd",
"tool_allowlist": ["ords.query.kb_select_ai_vpd"],
"router_mode": "direct",
"description": "KB VPD Select AI MCP; the current user's VPD bearer is sent only in the Authorization header."
}
```
환경 변수는 아래 두 값만 필요하다.
```dotenv
KB_MCP_BASE_URL=https://kb.cloud-handson.com
POC3_MCP_TIMEOUT_SECONDS=90
```
`endpoint_url`, `token_env`, `POC3_MCP_TOKEN`, `BACKOFFICE_MCP_ACCESS_TOKEN`은 KB MCP 선언에 두지 않는다. URL은 registry에 하드코딩하지 않고, VPD 토큰은 사용자별 요청에서만 받는다.
### 6.2 MCP 요청
모든 요청은 다음 헤더를 사용한다.
```http
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: <negotiated version>
Authorization: Bearer <current-user-vpd-token>
```
`tools/call` body의 `arguments`는 아래와 같이 제한한다.
```json
{
"name": "ords.query.kb_select_ai_vpd",
"arguments": {
"prompt": "담당 고객의 보험료 상세를 보여줘",
"limit": 50
}
}
```
경계 검증 규칙:
- tool name은 정확히 allowlist 값 하나와 일치해야 한다.
- `prompt`는 문자열이며 서버와 동일한 최대 길이를 적용한다.
- `limit`은 정수 `1..100`으로 clamp한다.
- 토큰은 공백·`Bearer ` prefix를 정규화한 뒤 헤더에만 넣는다.
- redirect는 허용하지 않는다. 다른 origin으로 Authorization이 전달되어서는 안 된다.
## 7. 함수 명세 (Function Specs)
| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? |
|------|-----------|----------------|------|------|-----------|-------|
| `resolve_kb_mcp_endpoint` | base URL과 고정 path를 안전하게 결합 | `(server, environ) -> str` | registry, env | HTTPS MCP URL | 누락/비정상 URL | 단순 |
| `validate_kb_mcp_server` | KB 선언의 provider·transport·allowlist를 검증 | `(mapping) -> McpServer` | registry row | typed server | 계약 위반 | 단순 |
| `mcp_headers` | 요청별 VPD bearer 헤더 생성 | `(token, version, session_id) -> dict` | 사용자 토큰 | 안전한 headers | 토큰 형식 오류 | 단순 |
| `discover_kb_tools` | initialize 및 tools/list 후 단일 도구를 검증 | `(server, token) -> McpDiscoveryResult` | endpoint, token | tool descriptor | auth/protocol 오류 | **복잡** |
| `call_kb_select_ai` | 고정 도구에 prompt·limit을 전달 | `(server, token, prompt, limit) -> result` | 사용자 요청 | tool result | auth/timeout/JSON-RPC 오류 | **복잡** |
| `to_public_mcp_error` | HTTP/JSON-RPC 오류를 안전한 UI 메시지로 변환 | `(exception) -> PublicMcpError` | 내부 오류 | 사용자 메시지 | 원문 노출 금지 | 단순 |
## 8. 흐름 / 알고리즘
1. UI는 `KB_MCP_BASE_URL`을 읽고 `/mcp`만 결합한다. registry의 임의 `endpoint_url`은 KB 서버에 허용하지 않는다.
2. 사용자가 VPD token preset 또는 일회성 Bearer를 선택한다. 토큰은 현재 실행 변수에만 유지한다.
3. client는 `initialize`를 보내고 서버가 반환한 protocol version과 선택 가능한 session ID를 검증한다.
4. `notifications/initialized`를 보낸 뒤 `tools/list`를 실행한다.
5. 응답 목록이 정확히 허용 도구를 포함하는지, 해당 input schema가 `prompt`, `limit` 계약에 맞는지 확인한다.
6. KB 서버는 단일 도구이므로 router model을 호출하지 않고 `ords.query.kb_select_ai_vpd`를 직접 선택한다.
7. `tools/call`은 prompt·clamp된 limit만 body에 넣고 VPD Bearer는 Authorization에만 넣는다.
8. 결과는 화면용 안전 projection만 SQLite에 저장한다. Authorization 헤더와 원문 token은 저장하지 않으며, 진단이 필요하면 단방향 token fingerprint만 별도 보존할 수 있다.
## 9. 엣지케이스 & 에러 처리
| 상황 | client 처리 | 서버 기대 동작 |
|------|-------------|----------------|
| 토큰 없음 | 호출 전 안내, 네트워크 요청 없음 | 해당 없음 |
| 토큰 위조·만료 | `401` → “토큰이 유효하지 않거나 만료됨” | `WWW-Authenticate` 포함 가능 |
| 유효하지만 권한 없음 | `403` → “이 사용자에게 조회 권한 없음” | VPD fail-closed 유지 |
| allowlist 밖 도구 | 호출 전 차단 | tools/call에서도 차단 |
| 429 | 안전하게 재시도하지 않고 잠시 후 재시도 안내 | rate limit 정책 적용 |
| timeout | tools/call 자동 재시도 금지 | request ID 기반 감사 추적 |
| session ID 미발급 | stateless POST로 진행 | session을 요구하지 않음 |
| session ID 발급 | 이후 요청에 `Mcp-Session-Id` 포함 | 세션 소유·만료 검증 |
| redirect | 즉시 실패 | Authorization 전달 금지 |
| Origin 불일치 | 브라우저 UI에 일반 오류 표시 | `403`으로 거부 |
## 10. 테스트 계획
- registry 단위 테스트
- `KB_MCP_BASE_URL`만으로 endpoint가 `https://kb.cloud-handson.com/mcp`가 되는지 검증
- KB registry에 `endpoint_url`, `token_env`, `custom_python`이 있으면 fail-closed 되는지 검증
- vector MCP 설정은 기존 형식으로 계속 로드되는지 검증
- HTTP transport 단위 테스트
- initialize/tools/list/tools/call 모두 Authorization header가 있고 JSON body에는 token key가 없는지 검증
- `401`, `403`, `429`, timeout, redirect, malformed JSON-RPC 응답을 안전한 메시지로 변환하는지 검증
- session header 반환/재전송 및 stateless fallback을 검증
- 통합 smoke test
- 허용된 VPD 사용자 토큰으로 `tools/list``tools/call` 성공
- 잘못된 토큰은 `401`, 타 사용자 권한은 `403`
- 설계사와 지점장 토큰으로 동일 질문을 실행해 VPD 행/컬럼 결과가 서로 다른지 확인
- 비밀정보 점검
- chat SQLite, Streamlit log, 예외 메시지에서 토큰 원문 검색 결과 0건
## 11. 리스크 & 대안 검토
- **선택**: KB MCP 전용 `kb_vpd_streamable_http` 선언을 도입하고, 로컬 8500용 `custom_python`과 분리한다. 외부 HTTPS/VPD Bearer 계약을 코드와 운영 화면에서 명확히 할 수 있다.
- **대안 1 — 기존 `custom_python` 재사용**: 동작은 시킬 수 있으나 provider 이름과 endpoint 제약이 실제 KB 서버와 맞지 않아 로컬 MCP와 외부 VPD MCP가 섞인다.
- **대안 2 — 고정 MCP access token 사용**: 구현은 간단하지만 모든 사용자가 동일 VPD 주체가 되어 데이터 권한 분리가 무너진다. 채택하지 않는다.
- **대안 3 — 즉시 OAuth 2.1 전환**: 표준 상호운용성에는 유리하지만 현재 데모의 VPD token 발급·검증 체계를 대체하므로 별도 인증 서버 설계가 필요하다.
- 롤백: 새 registry 선언을 비활성화하고 기존 KB 선언을 복원한다. DB VPD 정책·ORDS endpoint·토큰 데이터는 변경하지 않는다.
## 12. 미해결 질문 (Open Questions)
- VPD Backoffice MCP endpoint가 현재 지원할 MCP protocol version을 어떤 값으로 공식 고정할지 결정이 필요하다.
- Streamable HTTP의 GET/SSE 및 `Mcp-Session-Id`를 완전 지원할지, stateless POST profile로 운영할지 결정이 필요하다.
- 데모 이후 사용자 VPD bearer를 OAuth 2.1 access token으로 전환할지, 현 토큰을 resource-server token으로 계속 운영할지 결정이 필요하다.
- chat 대화 이력의 `basis_json`/`details_json`에 민감 데이터 보존 기간과 삭제 정책을 별도로 정해야 한다.

View File

@@ -0,0 +1,83 @@
# VPD·ASO 권한 운영 페이지별 페르소나 리뷰
> 상태: Review only
> 작성일: 2026-07-13
> 범위: 현재 Spring Boot 백오피스 화면을 기준으로, 구현 변경 없이 페이지별 개선 의견만 정리
## 1. 이번 리뷰의 전제
이 백오피스의 핵심 목적은 Oracle DB에서 VPD와 ASO(Data Redaction)를 조합해 사용자별 데이터 접근 범위를 운영하는 것이다.
- VPD는 행을 남기거나 제외한다. 토큰이 직접 VPD 함수에 전달되는 것이 아니라, 토큰 검증 후 세션 컨텍스트(`CB_AGENT_CTX`)에 들어간 값이 VPD 필터의 조건으로 쓰인다.
- ASO는 컬럼 값을 원문 또는 마스킹 값으로 반환한다. ASO 정책은 세션 컨텍스트의 `MR_<column_id>` 같은 값을 보고 원문 표시 여부를 판단한다.
- 백오피스에서 등록하는 접근 규칙은 최종 SQL 그 자체가 아니라, VPD 필터가 읽어 SQL predicate로 바꾸는 매핑 데이터다.
- 일반 운영자는 SQL 함수 구조보다 “이 사용자에게 어떤 테이블/행/컬럼이 어떻게 보이는가”를 먼저 이해해야 한다.
## 2. 리뷰 페르소나
| 페르소나 | 관심사 | 실패로 보는 상황 |
|---|---|---|
| 일반 사용자 | 검증 토큰으로 실제 결과를 확인하고 싶다 | VPD, ASO, ORDS, MCP 용어 때문에 무엇을 눌러야 할지 모름 |
| 운영 관리자 | 사용자·역할·권한·마스킹 설정을 안전하게 바꾸고 싶다 | 설정 변경의 영향 범위와 검증 방법이 분리되어 있음 |
| 적용 담당자 | 업무 규칙을 DB 적용 가능한 설정으로 옮기고 싶다 | “본인계약”, “채널내 전체”, “원문 허용”이 실제 필터/컨텍스트와 어떻게 연결되는지 안 보임 |
| DB 관리자 | DB에 어떤 정책과 스크립트가 적용됐는지 확인하고 싶다 | 백오피스 설정과 DBMS_RLS/DBMS_REDACT 실제 상태가 맞는지 증적이 부족함 |
## 3. 공통 개선 원칙
1. 기본 화면은 “현재 상태, 주요 행동, 검증 버튼” 중심으로 둔다.
2. SQL, 패키지, ORDS, VPD 함수, Redaction expression은 Advanced 또는 도움말로 보낸다.
3. 모든 권한 화면은 `업무 표현 → 저장 설정 → VPD/ASO 해석 → 검증 결과` 순서로 설명한다.
4. VPD와 ASO를 섞어 말하지 않는다.
- VPD: 어떤 행을 볼 수 있는가.
- ASO: 허용된 행의 어떤 컬럼을 원문으로 볼 수 있는가.
5. 권한 설정 화면에서는 “이 조건이 그대로 DB에 붙는다”가 아니라 “이 설정값을 필터가 읽어 WHERE 조건을 만든다”라고 표현한다.
6. 간단한 설정을 기본으로 두고, 조건식·정책명·패키지명·스키마명은 Advanced에서 확인하게 한다.
## 4. 페이지별 리뷰 파일
| 영역 | 페이지 | 리뷰 파일 |
|---|---|---|
| 시작 | 로그인 | [00-login.md](pages/00-login.md) |
| 시작 | 대시보드 | [01-dashboard.md](pages/01-dashboard.md) |
| 권한 주체 | 사용자 | [02-users.md](pages/02-users.md) |
| 권한 주체 | 그룹 | [03-groups.md](pages/03-groups.md) |
| 권한 주체 | 역할 | [04-roles.md](pages/04-roles.md) |
| 권한 설정 | 접근 규칙 | [05-permissions.md](pages/05-permissions.md) |
| 권한 설정 | 원문 조회 허용 사용자 | [06-user-masking-rules.md](pages/06-user-masking-rules.md) |
| 권한 설정 | 사용자별 접근 확인 | [07-effective-matrix.md](pages/07-effective-matrix.md) |
| 보호·검증 | 보호 상태 | [08-vpd-policies.md](pages/08-vpd-policies.md) |
| 보호·검증 | 마스킹 규칙 | [09-masking-rules.md](pages/09-masking-rules.md) |
| 보호·검증 | 검증 세션 | [10-tokens.md](pages/10-tokens.md) |
| 보호·검증 | 접근 검증 | [11-probe.md](pages/11-probe.md) |
| 연동 | 조회 대상 | [12-objects.md](pages/12-objects.md) |
| 연동 | 정형 데이터 조회 | [13-structured-data.md](pages/13-structured-data.md) |
| 연동 | 조회 연동 | [14-ords-handlers.md](pages/14-ords-handlers.md) |
| 연동 | 지식 검색 | [15-vector-knowledge.md](pages/15-vector-knowledge.md) |
| 연동 | 대화형 검색 | [16-mcp-chatbot.md](pages/16-mcp-chatbot.md) |
| 연동 | 검색 해석 | [17-mcp-reasoning.md](pages/17-mcp-reasoning.md) |
| 연동 | MCP 서비스 | [18-mcp-sse.md](pages/18-mcp-sse.md) |
| 연동 | 연동 점검 | [19-mcp-client-demo.md](pages/19-mcp-client-demo.md) |
| 운영 | 운영 현황 | [20-operation-status.md](pages/20-operation-status.md) |
| 관리자 | VPD 필터 구조 | [21-vpd-filter-runtime.md](pages/21-vpd-filter-runtime.md) |
| 관리자 | DB 메타데이터 | [22-schema-metadata.md](pages/22-schema-metadata.md) |
| 관리자 | 보안 SQL 스크립트 | [23-security-sql-scripts.md](pages/23-security-sql-scripts.md) |
| 관리자 | 고급 접근 조건 | [24-vpd-filter-policies.md](pages/24-vpd-filter-policies.md) |
| 관리자 | 시스템 설정 | [25-settings.md](pages/25-settings.md) |
| 관리자 | DB 준비 상태 | [26-settings-database.md](pages/26-settings-database.md) |
## 5. 적용 우선순위 제안
| 우선순위 | 대상 | 이유 |
|---|---|---|
| P0 | 접근 규칙, 마스킹 규칙, 원문 조회 허용 사용자, 접근 검증 | 사용자가 VPD/ASO의 차이와 실제 적용 방식을 가장 많이 혼동하는 지점 |
| P0 | 보호 상태, 운영 현황 | DB 실제 적용 상태와 백오피스 설정 상태를 구분해야 장애 판단이 가능 |
| P1 | 사용자, 그룹, 역할, 사용자별 접근 확인 | 권한 주체와 상속 경로를 업무 담당자가 이해하기 쉽게 해야 함 |
| P1 | DB 메타데이터, 보안 SQL 스크립트, VPD 필터 구조 | 적용 담당자와 DB 관리자의 증적 확인 화면 |
| P2 | MCP/Select AI/Vector 연동 화면 | 기능 자체보다 권한이 적용된 호출 흐름과 지연 원인을 보여주는 방향으로 정리 |
## 6. 구현 전 확인할 설계 판단
- ASO는 컬럼 마스킹만 담당하고, 행 접근은 VPD만 담당한다는 원칙을 화면 문구와 메뉴명에 일관되게 반영한다.
- “원문 조회 허용 사용자”는 현재 사용자 단위 UNMASK 예외 중심이다. 향후 “내 담당 고객은 원문, 타인은 마스킹” 같은 조건부 원문 표시가 필요하면 VPD로 행 범위를 먼저 제한하고 ASO 컨텍스트 계산 방식을 확장해야 한다.
- 지점장 집계 요구는 ASO 마스킹 컬럼에 직접 `SUM`을 걸어 해결한다고 가정하면 안 된다. 집계 전용 trusted path나 별도 검증 가능한 API 설계가 필요하다.
- Select AI용 메타데이터 화면은 자연어 질의 품질에 직접 영향을 주므로, 단순 주석 편집이 아니라 “이 컬럼이 어떤 업무 의미인지”를 사람이 이해하고 보강하는 화면으로 다뤄야 한다.

View File

@@ -0,0 +1,28 @@
# 로그인 페이지 리뷰
- URL: `/login`
- 현재 목적: 백오피스 계정으로 VPD 권한 운영 콘솔에 진입한다.
- VPD/ASO 관련성: 로그인 계정은 백오피스 운영 권한이고, 검증 토큰의 업무 사용자와 다르다.
## 페르소나 의견
- 일반 사용자: 로그인한 계정과 나중에 발급하는 VPD 검증 토큰의 사용자가 같은 것인지 헷갈릴 수 있다.
- 운영 관리자: 관리자 계정과 guest 계정의 차이가 첫 화면에서 보이면 안전하다.
- 적용 담당자: “운영 콘솔 로그인”과 “DB 접근 토큰 검증”이 다른 단계임을 알아야 한다.
- DB 관리자: 이 로그인은 DB 계정 로그인이 아니라 애플리케이션 계정이라는 점이 명확해야 한다.
## 가벼운 개선
1. 로그인 카드 하단에 “이 계정은 백오피스 화면 접근용이며, 실제 VPD 검증은 검증 세션 토큰으로 수행합니다.” 문구를 추가한다.
2. guest 계정은 읽기 전용임을 로그인 후 배너뿐 아니라 로그인 화면 안내에도 짧게 표시한다.
3. 로그인 유지 체크박스는 “이 브라우저에서 유지”처럼 보안 범위를 명확히 쓴다.
## Advanced로 둘 내용
- 세션 쿠키 만료 정책
- 권한별 접근 가능 URL
- guest read-only 서버 정책
## 우선순위
P2. 혼동을 줄이는 문구 개선이 중심이고, 권한 계산 로직에는 영향이 없다.

View File

@@ -0,0 +1,28 @@
# 대시보드 리뷰
- URL: `/`
- 현재 목적: 권한 운영 흐름, 메뉴 안내, 현재 검증 가능한 데이터를 보여준다.
- VPD/ASO 관련성: VPD 행 필터와 ASO 컬럼 마스킹의 전체 업무 흐름을 처음 설명하는 화면이다.
## 페르소나 의견
- 일반 사용자: “권한 관리 → 보호·검증 → 연동” 흐름은 좋지만, VPD와 ASO의 차이는 한 문장으로 먼저 보여야 한다.
- 운영 관리자: 오늘 해야 할 일, 예를 들면 “접근 규칙 수정”, “검증 세션 발급”, “DB 적용 상태 확인”이 바로 보여야 한다.
- 적용 담당자: 업무 규칙이 필터 조건으로 변환되는 구조가 대시보드에서 먼저 잡혀야 한다.
- DB 관리자: 실제 DB 정책 상태와 백오피스 설정 상태가 어디에서 확인되는지 바로 연결되어야 한다.
## 가벼운 개선
1. 상단에 `VPD = 행 제한`, `ASO = 컬럼 마스킹`, `Probe = 실제 결과 검증` 3개 요약 카드를 둔다.
2. “권한 설정값은 VPD 함수가 읽어 WHERE 조건으로 바꿉니다”라는 짧은 설명을 접근 규칙 카드에 붙인다.
3. “현재 DB 적용 상태” 요약을 보호 상태 또는 운영 현황으로 바로 연결한다.
## Advanced로 둘 내용
- VPD 함수 내부 흐름
- ASO Data Redaction expression
- ORDS/MCP 호출 구조
## 우선순위
P1. 홈에서 전체 모델을 정확히 잡으면 다른 화면의 설명량을 줄일 수 있다.

View File

@@ -0,0 +1,28 @@
# 사용자 페이지 리뷰
- URL: `/users`
- 현재 목적: 백오피스 권한 모델의 사용자 생성, 목록 확인, 사용자 역할 부여를 수행한다.
- VPD/ASO 관련성: 사용자는 VPD 필터가 읽는 역할·권한의 출발점이며, ASO 원문 허용 설정의 대상이 된다.
## 페르소나 의견
- 일반 사용자: 여기의 사용자가 실제 보험 설계사/지점장인지, 백오피스 계정인지 구분하기 어렵다.
- 운영 관리자: 사용자 선택 후 “이 사용자가 최종적으로 어떤 역할과 보호 객체를 갖는지”를 바로 보고 싶다.
- 적용 담당자: `KB_STAKEHOLDERS`와 토큰 주체의 관계가 사용자 화면에서 보이지 않으면 업무 사용자 매핑을 놓칠 수 있다.
- DB 관리자: 사용자 추가가 DB 계정 생성이 아니라 애플리케이션 권한 데이터 추가라는 점이 필요하다.
## 가벼운 개선
1. 사용자 상세에 “직접 역할, 그룹 상속 역할, 원문 허용 컬럼, 발급 가능 토큰” 요약을 추가한다.
2. VPD 데모 사용자라면 `KB_STAKEHOLDERS.USER_ID` 매핑 상태를 배지로 표시한다.
3. 역할 부여 후 바로 “사용자별 접근 확인”과 “접근 검증”으로 이동하는 버튼을 둔다.
## Advanced로 둘 내용
- 내부 테이블명 `CB_APP_USER`, `CB_USER_ROLE`
- 그룹 상속 SQL
- DB 계정과 애플리케이션 사용자의 차이 상세
## 우선순위
P1. 권한 주체를 이해해야 접근 규칙과 토큰 검증의 혼동이 줄어든다.

View File

@@ -0,0 +1,27 @@
# 그룹 페이지 리뷰
- URL: `/groups`
- 현재 목적: 그룹 생성, 그룹 사용자 연결, 그룹 역할 연결을 관리한다.
- VPD/ASO 관련성: 그룹 역할은 VPD 필터의 effective role 계산에 포함된다.
## 페르소나 의견
- 일반 사용자: 그룹이 실제 조직 지점인지, 권한 묶음인지 알기 어렵다.
- 운영 관리자: 그룹에 역할을 추가하면 몇 명의 사용자가 영향을 받는지 먼저 봐야 한다.
- 적용 담당자: 지점·채널 같은 업무 조직과 권한 그룹의 차이를 표시해야 오해가 줄어든다.
- DB 관리자: 그룹은 VPD predicate에 직접 들어가는 값이 아니라 역할 상속 경로라는 점이 필요하다.
## 가벼운 개선
1. 그룹 선택 시 영향 사용자 수와 상속 역할 목록을 상단에 고정한다.
2. “그룹은 역할을 묶어 부여하는 운영 단위입니다. 지점/채널 조건은 접근 규칙 또는 stakeholder context에서 평가됩니다.” 문구를 추가한다.
3. 그룹 역할 변경 후 사용자별 접근 확인으로 이동시키는 CTA를 제공한다.
## Advanced로 둘 내용
- `CB_USER_GROUP`, `CB_GROUP_ROLE` 조인 구조
- active group만 effective role에 포함되는 조건
## 우선순위
P1. 운영 관리자가 대량 영향 변경을 안전하게 이해하는 데 필요하다.

View File

@@ -0,0 +1,28 @@
# 역할 페이지 리뷰
- URL: `/roles`
- 현재 목적: 역할 생성과 역할 목록 관리를 수행한다.
- VPD/ASO 관련성: 역할은 접근 규칙과 원문 조회 예외를 연결하는 핵심 단위다.
## 페르소나 의견
- 일반 사용자: 역할 이름만 봐서는 어떤 데이터 범위를 의미하는지 알기 어렵다.
- 운영 관리자: 역할 삭제나 이름 변경 전에 연결된 사용자·그룹·권한 수가 먼저 보여야 한다.
- 적용 담당자: “설계사”, “지점장”, “VPD 관리자” 같은 업무 역할과 DB 정책 적용 결과를 같이 보고 싶다.
- DB 관리자: 역할은 Oracle role이 아니라 백오피스 권한 테이블의 역할이라는 점이 명확해야 한다.
## 가벼운 개선
1. 역할 목록에 연결 사용자 수, 그룹 수, 접근 규칙 수, 원문 허용 규칙 수를 표시한다.
2. 역할 상세에서 “이 역할이 만드는 VPD/ASO 효과”를 업무 문장으로 요약한다.
3. 삭제 버튼은 영향 상세 확인 후 노출한다.
## Advanced로 둘 내용
- 내부 role_id
- 권한 테이블 조인 구조
- Oracle DB role과의 차이
## 우선순위
P1. 접근 규칙의 주체가 역할이므로 사용자 영향도를 쉽게 보여줘야 한다.

View File

@@ -0,0 +1,36 @@
# 접근 규칙 페이지 리뷰
- URL: `/permissions`
- 현재 목적: 역할별 보호 객체 접근 규칙을 등록하고 관리한다.
- VPD/ASO 관련성: 이 화면의 설정은 VPD 필터가 읽어 대상 테이블의 WHERE predicate로 변환한다.
## 페르소나 의견
- 일반 사용자: `CUST_ID = token stakeholder` 같은 축약 표현은 실제 적용 방식을 이해하기 어렵다.
- 운영 관리자: 저장한 조건이 “최종 SQL”인지 “필터가 읽는 설정값”인지 명확해야 한다.
- 적용 담당자: 업무 조건을 고르는 방식이어야 한다. 예: 본인 계약, 내 채널 계약, 전체 허용, 정적 SQL 조건.
- DB 관리자: STATIC_SQL 같은 고급 조건은 검증·차단 규칙과 함께 보여야 한다.
## 가벼운 개선
1. 기본 입력은 업무 조건 선택형으로 둔다.
- 전체 행 허용
- 본인 계약
- 내 채널 계약
- 담당 고객
- 내 채널 고객
2. 각 조건 옆에 “필터 변환 예시”를 접힌 형태로 보여준다.
- 예: 본인 계약 → `FC_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')`
3. 목록에는 저장된 rule_type보다 업무 문장을 먼저 보여준다.
4. STATIC_SQL은 Advanced 영역으로 이동하고, “검증된 컬럼 조건만 허용”을 명시한다.
## Advanced로 둘 내용
- rule_type, rule_column, rule_value 원본
- 생성되는 VPD predicate 예시
- safe_static_predicate 방어 규칙
- ALLOW/DENY 결합 방식
## 우선순위
P0. 사용자가 가장 많이 오해하는 화면이다. “설정값을 필터가 WHERE로 바꾼다”는 표현을 반드시 강화해야 한다.

View File

@@ -0,0 +1,28 @@
# 원문 조회 허용 사용자 페이지 리뷰
- URL: `/user-masking-rules`
- 현재 목적: ASO 마스킹 대상 컬럼에 대해 특정 사용자에게 원문 조회 예외를 준다.
- VPD/ASO 관련성: VPD로 허용된 행 안에서 특정 컬럼을 원문으로 볼 수 있는지 결정한다.
## 페르소나 의견
- 일반 사용자: “원문 허용”이 행 접근 권한까지 주는 것처럼 보일 수 있다.
- 운영 관리자: 특정 사용자에게 컬럼 원문을 허용하면 어떤 테이블/컬럼에 영향이 있는지 바로 봐야 한다.
- 적용 담당자: “자기 고객이면 원문, 타 고객이면 마스킹” 같은 조건부 요구는 현재 단순 UNMASK 예외와 다르다는 점이 필요하다.
- DB 관리자: 이 설정은 ASO expression이 읽는 세션 context를 만드는 입력값이라는 설명이 필요하다.
## 가벼운 개선
1. 상단 문구를 “행 접근은 VPD가 결정하고, 이 화면은 허용된 행의 컬럼 원문 표시만 결정합니다.”로 고정한다.
2. 사용자 선택 시 “이 사용자가 VPD로 볼 수 있는 행 범위” 링크를 접근 검증으로 연결한다.
3. 단순 원문 허용과 조건부 원문 허용의 차이를 안내한다.
## Advanced로 둘 내용
- `set_masking_rule_context(user_id)` 흐름
- `MR_<column_id>` 세션 컨텍스트
- `DBMS_REDACT.UPDATE_POLICY_EXPRESSION` 적용 방식
## 우선순위
P0. VPD와 ASO 권한을 섞어 이해하면 운영 사고로 이어질 수 있다.

View File

@@ -0,0 +1,28 @@
# 사용자별 접근 확인 페이지 리뷰
- URL: `/effective-matrix`
- 현재 목적: 사용자별 최종 역할과 보호 객체 접근 현황을 확인한다.
- VPD/ASO 관련성: VPD가 사용할 effective role과 접근 규칙의 근거를 설명한다.
## 페르소나 의견
- 일반 사용자: 이 화면이 실제 DB 조회 결과인지, 설정상 예상 결과인지 구분이 필요하다.
- 운영 관리자: 사용자 한 명을 선택하면 직접 역할, 그룹 역할, 최종 접근 객체가 한 흐름으로 보여야 한다.
- 적용 담당자: “왜 이 사용자가 이 객체를 볼 수 있는가”의 근거 경로가 필요하다.
- DB 관리자: 실제 DB 정책 적용 여부는 별도 화면이라는 구분이 필요하다.
## 가벼운 개선
1. 상단에 “이 화면은 설정 기반 예상 권한입니다. 실제 조회 결과는 접근 검증에서 확인합니다.”를 표시한다.
2. 사용자별 카드에 `직접 역할 → 그룹 상속 → 최종 역할 → 접근 객체` 타임라인을 둔다.
3. 각 보호 객체에서 `접근 검증`으로 바로 이동하게 한다.
## Advanced로 둘 내용
- effective role SQL
- ALLOW/DENY 결합 로직
- 그룹 active 여부 반영 방식
## 우선순위
P1. 운영자가 변경 전후 영향 확인에 사용하는 중심 화면으로 만들 필요가 있다.

View File

@@ -0,0 +1,28 @@
# 보호 상태 페이지 리뷰
- URL: `/vpd-policies`
- 현재 목적: 보호 대상 객체와 DB에 적용된 VPD 정책 상태를 확인하고 연결한다.
- VPD/ASO 관련성: VPD 함수와 테이블 연결 상태를 DBMS_RLS 기준으로 확인하는 화면이다.
## 페르소나 의견
- 일반 사용자: “보호 적용”이 행 필터만 의미하는지, 컬럼 마스킹도 포함하는지 혼동할 수 있다.
- 운영 관리자: 백오피스 설정은 있는데 DB 정책이 빠진 상태를 쉽게 알아야 한다.
- 적용 담당자: 보호 객체별로 “설정 있음 / DB 적용됨 / 검증 성공” 3단계를 나눠 보고 싶다.
- DB 관리자: policy_name, function_schema, function_name, enable 상태가 증적으로 필요하다.
## 가벼운 개선
1. 배지를 `설정됨`, `DB VPD 적용`, `최근 검증 성공`으로 분리한다.
2. 컬럼 마스킹은 별도 ASO 상태임을 마스킹 규칙 화면으로 연결한다.
3. 보호 연결 버튼 옆에 “연결 후 접근 검증 필요” 문구를 둔다.
## Advanced로 둘 내용
- `DBMS_RLS.ADD_POLICY`/`DROP_POLICY`
- policy function owner
- object schema와 policy schema
## 우선순위
P0. “설정은 했는데 DB에 적용됐는지”를 판단하는 핵심 운영 화면이다.

View File

@@ -0,0 +1,33 @@
# 마스킹 규칙 페이지 리뷰
- URL: `/masking-rules`
- 현재 목적: 사전 정의 마스킹 방식, 마스킹 대상 컬럼, 컬럼별 기본 규칙, DB ASO 정책 동기화를 관리한다.
- VPD/ASO 관련성: ASO Data Redaction 정책과 컬럼 원문/마스킹 판단의 중심 화면이다.
## 페르소나 의견
- 일반 사용자: ASO가 행 접근을 막는 기능으로 오해될 수 있다.
- 운영 관리자: 컬럼 연결을 해제했을 때 DB Redaction 정책도 같이 해제됐는지 확인해야 한다.
- 적용 담당자: 마스킹 방식과 대상 컬럼 등록, 사용자 원문 허용이 서로 어떤 순서인지 알아야 한다.
- DB 관리자: DBMS_REDACT 정책명, 적용 expression, 활성 여부가 필요하다.
## 가벼운 개선
1. 화면 상단에 3단계 흐름을 둔다.
- 대상 컬럼 등록
- 기본 마스킹 방식 연결
- 원문 조회 허용 사용자 지정
2. 각 컬럼 행에 `백오피스 연결 상태``DB ASO 적용 상태`를 따로 표시한다.
3. “VPD로 허용된 행 안에서만 마스킹 여부가 의미 있습니다.” 문구를 반복 노출한다.
4. 숫자 컬럼 마스킹과 집계의 관계는 도움말로 분리한다. `SUM`이 원문 합계를 보장한다고 표현하면 안 된다.
## Advanced로 둘 내용
- Redaction function type
- policy expression
- `MR_<column_id>` context
- DBMS_REDACT 오류 코드와 동기화 로그
## 우선순위
P0. ASO 설정의 실제 DB 적용 여부를 화면에서 바로 판단할 수 있어야 한다.

View File

@@ -0,0 +1,32 @@
# 검증 세션 페이지 리뷰
- URL: `/tokens`
- 현재 목적: 이해관계자 기준 Bearer token을 발급하고 발급 이력을 확인한다.
- VPD/ASO 관련성: 토큰은 세션 context를 만들기 위한 입력이고, VPD/ASO는 그 context를 읽는다.
## 페르소나 의견
- 일반 사용자: 발급된 토큰이 어디에 쓰이는지 한 번 더 안내가 필요하다.
- 운영 관리자: 어떤 사용자/이해관계자/역할로 토큰이 발급됐는지 확인해야 한다.
- 적용 담당자: stakeholder의 role, channel, user_id가 context로 어떻게 들어가는지 알아야 한다.
- DB 관리자: 토큰 원문 보관 여부와 만료 정책이 중요하다.
## 가벼운 개선
1. 토큰 발급 결과에 “이 토큰으로 세팅되는 context” 요약을 표시한다.
- `USER_ID`
- `STAKEHOLDER_USER_ID`
- `STAKEHOLDER_ROLE`
- `STAKEHOLDER_CHANNEL`
2. 발급 직후 접근 검증으로 넘길 때 토큰을 자동 선택한다.
3. 토큰 오류 시 “권한이 없거나 만료된 토큰”처럼 사용자 행동 기준 오류를 표시한다.
## Advanced로 둘 내용
- bearer key 저장 방식
- context package 내부 함수
- 만료/폐기 SQL
## 우선순위
P0. 토큰이 권한 자체가 아니라 context 설정 입력이라는 점을 명확히 해야 한다.

View File

@@ -0,0 +1,29 @@
# 접근 검증 페이지 리뷰
- URL: `/probe`
- 현재 목적: 토큰과 보호 객체를 사용해 실제 ORDS/DB 조회 결과를 확인한다.
- VPD/ASO 관련성: VPD 행 필터와 ASO 컬럼 마스킹이 실제 결과에 어떻게 반영됐는지 확인하는 최종 검증 화면이다.
## 페르소나 의견
- 일반 사용자: “내가 이 사용자라면 실제로 무엇을 볼 수 있나”만 빠르게 보고 싶다.
- 운영 관리자: 설정 변경 후 검증 결과와 이전 결과를 비교하고 싶다.
- 적용 담당자: 결과에 VPD predicate와 ASO 마스킹 여부가 같이 보이면 원인 파악이 쉽다.
- DB 관리자: 재현 SQL, 실행 컨텍스트, 적용 정책 증적이 필요하다.
## 가벼운 개선
1. 결과를 `세션 context`, `VPD 행 결과`, `ASO 컬럼 마스킹`, `원본 응답` 4개 탭으로 나눈다.
2. 잘못된 토큰은 DB 오류처럼 보이지 않게 “토큰 권한 없음/만료/인식 불가”로 반환한다.
3. 마스킹된 컬럼은 결과 테이블에서 별도 아이콘이나 툴팁으로 표시한다.
## Advanced로 둘 내용
- SQL trace
- VPD predicate
- ORDS handler source
- DB cursor 증적
## 우선순위
P0. 이 화면은 모든 설정 변경의 성공 기준이다.

View File

@@ -0,0 +1,28 @@
# 조회 대상 페이지 리뷰
- URL: `/objects`
- 현재 목적: ORDS 조회 대상과 보호 객체 후보를 등록하고 관리한다.
- VPD/ASO 관련성: 보호 객체로 등록된 테이블이 VPD/ASO 정책 적용과 검증의 단위가 된다.
## 페르소나 의견
- 일반 사용자: 조회 대상, 보호 대상, ORDS handler의 차이를 구분하기 어렵다.
- 운영 관리자: 새 테이블을 등록하면 다음에 무엇을 해야 하는지 안내가 필요하다.
- 적용 담당자: 업무명, 테이블명, 키 컬럼, 권한 조건 후보를 같이 관리해야 한다.
- DB 관리자: 스키마와 객체명 검증, 실제 존재 여부, 권한 여부가 필요하다.
## 가벼운 개선
1. 객체 등록 후 다음 행동을 `보호 연결`, `접근 규칙 추가`, `접근 검증`으로 안내한다.
2. 목록에 업무명과 DB 객체명을 같이 표시하되, 업무명을 먼저 보여준다.
3. ASO 컬럼 대상 등록은 마스킹 규칙 화면으로 명확히 연결한다.
## Advanced로 둘 내용
- ORDS module/template/handler 연결
- schema owner
- 테이블 존재 확인 SQL
## 우선순위
P1. 신규 테이블 온보딩 흐름을 단순하게 만드는 것이 핵심이다.

View File

@@ -0,0 +1,31 @@
# 정형 데이터 조회 페이지 리뷰
- URL: `/structured-data`
- 현재 목적: KB 원장성 테이블을 관리자용으로 미리 조회한다.
- VPD/ASO 관련성: 실제 사용자별 적용 결과가 아니라 관리자용 원장 미리보기라는 점이 중요하다.
## 페르소나 의견
- 일반 사용자: 여기 결과가 본인 토큰 기준인지 관리자 원본 기준인지 헷갈릴 수 있다.
- 운영 관리자: 데모 데이터 구조를 확인하는 용도로는 좋지만, 권한 검증과 분리되어야 한다.
- 적용 담당자: 각 테이블의 업무 의미, 주요 조인 키, VPD 조건 후보가 같이 보여야 한다.
- DB 관리자: 원장 데이터 조회가 마스킹 정책을 우회하는 관리자 조회인지 표시가 필요하다.
## 가벼운 개선
1. 상단에 “관리자용 데이터 미리보기이며, 사용자별 결과는 접근 검증에서 확인합니다.”를 더 강하게 표시한다.
2. 각 테이블에 권한 기준 컬럼을 표시한다.
- 고객: `CUST_ID`
- 계약: `FC_ID`, `FC_CHANNEL`, `CUST_ID`
- 보상/외부보유: `CUST_ID`
3. 접근 검증으로 바로 이동하는 버튼을 둔다.
## Advanced로 둘 내용
- 전체 컬럼 목록
- 샘플 SQL
- 테이블 조인 구조
## 우선순위
P1. 관리자 미리보기와 사용자별 VPD 결과의 차이를 분명히 해야 한다.

View File

@@ -0,0 +1,29 @@
# 조회 연동 페이지 리뷰
- URL: `/ords-handlers`
- 현재 목적: ORDS handler와 source를 확인한다.
- VPD/ASO 관련성: 외부 호출이 어떤 DB 세션에서 context를 세팅하고 조회하는지 확인하는 기술 화면이다.
## 페르소나 의견
- 일반 사용자: ORDS handler source는 기본 화면에서 볼 필요가 거의 없다.
- 운영 관리자: endpoint 상태와 보호 객체 연결 여부가 먼저 필요하다.
- 적용 담당자: handler가 토큰을 받아 context를 세팅한 뒤 조회하는 흐름이 중요하다.
- DB 관리자: handler source와 DB package, 정책 적용 대상이 증적으로 필요하다.
## 가벼운 개선
1. 기본 화면은 endpoint, method, 보호 객체, 최근 검증 상태만 보여준다.
2. source와 수정 기능은 Advanced 상세로 접는다.
3. handler가 VPD/ASO를 우회하지 않는 이유를 짧게 표시한다.
## Advanced로 둘 내용
- ORDS source
- `set_vpd_context` 호출
- SQL trace
- handler 재배포 절차
## 우선순위
P1. 운영 화면과 개발자 화면의 밀도를 분리해야 한다.

View File

@@ -0,0 +1,28 @@
# 지식 검색 페이지 리뷰
- URL: `/vector-knowledge`
- 현재 목적: 지식자료 등록, 접근 정책 설정, 권한 기반 검색을 수행한다.
- VPD/ASO 관련성: 정형 VPD/ASO와 달리 벡터 지식 검색은 문서 단위 접근 정책과 RAG 검색 품질을 다룬다.
## 페르소나 의견
- 일반 사용자: 정형 데이터 권한과 벡터 검색 권한이 같은 방식인지 헷갈릴 수 있다.
- 운영 관리자: 자료 등록, 접근 정책, 검색 검증이 한 화면에 섞이면 운영 순서가 흐려진다.
- 적용 담당자: 상품/회사/약관 메타데이터가 부족하면 RAG 결과가 경쟁사 근거로 치우칠 수 있다.
- DB 관리자: VPD/ASO가 직접 적용되는 테이블 조회와 벡터 검색 정책을 구분해야 한다.
## 가벼운 개선
1. `자료 등록`, `접근 정책`, `검색 검증`을 탭 또는 단계로 분리한다.
2. 검색 결과에 “정형 MCP 결과 없음 / 벡터 근거만 있음” 같은 출처 구분을 표시한다.
3. 상품 필터와 문서 메타데이터 품질 점검 링크를 DB 메타데이터 화면과 연결한다.
## Advanced로 둘 내용
- embedding 상태
- hybrid rerank 파라미터
- vector table/source table 구조
## 우선순위
P2. VPD/ASO 핵심 화면보다 후순위지만, MCP 질의 품질에는 중요하다.

View File

@@ -0,0 +1,29 @@
# 대화형 검색 페이지 리뷰
- URL: `/mcp-chatbot`
- 현재 목적: 자연어 질의로 MCP/Select AI/Vector 검색을 호출한다.
- VPD/ASO 관련성: 자연어 질의라도 정형 DB 호출에는 VPD/ASO context가 적용되어야 한다.
## 페르소나 의견
- 일반 사용자: 질문 입력과 답변이 중심이어야 하고, 도구명은 보조 정보여야 한다.
- 운영 관리자: 어떤 토큰으로 어떤 도구가 호출됐는지 알 수 있어야 한다.
- 적용 담당자: Select AI가 잘못된 SQL을 만들면 테이블/컬럼 comment 보강으로 이어져야 한다.
- DB 관리자: DB 오류, 모델 지연, VPD 차단, ASO 마스킹을 구분해야 한다.
## 가벼운 개선
1. 결과를 `답변`, `호출 도구`, `정형 데이터 결과`, `근거 문서`, `오류 원인`으로 분리한다.
2. Select AI 호출 시간이 길면 모델/profile/재시도 여부를 표시한다.
3. “권한 때문에 안 보임”과 “질의 생성 실패”를 다른 오류로 보여준다.
## Advanced로 둘 내용
- MCP tool JSON
- Select AI showprompt/showsql
- LLM profile
- raw ORDS response
## 우선순위
P2. 데모 품질에는 중요하지만, 먼저 권한 운영 화면을 정리한 뒤 다루는 것이 좋다.

View File

@@ -0,0 +1,29 @@
# 검색 해석 페이지 리뷰
- URL: `/mcp-reasoning`
- 현재 목적: MCP 도구 선택과 권한 결과 해석을 확인한다.
- VPD/ASO 관련성: 자연어 질의가 어떤 protected tool로 라우팅되고, 어떤 토큰으로 실행됐는지 설명한다.
## 페르소나 의견
- 일반 사용자: “왜 이 도구가 선택됐는가”를 업무 문장으로 알고 싶다.
- 운영 관리자: 실패 시 정형 MCP, 벡터 검색, Select AI 중 어느 구간이 문제인지 봐야 한다.
- 적용 담당자: 라우팅 결과와 스키마 메타데이터 부족을 연결해야 한다.
- DB 관리자: 실제 DB 호출과 VPD/ASO 적용 여부를 증적으로 보고 싶다.
## 가벼운 개선
1. 결과 타임라인을 `질문 → 도구 선택 → 토큰 context → DB/RAG 호출 → 응답` 순서로 표시한다.
2. 각 단계의 시간과 오류 원인을 표시한다.
3. Select AI prompt 또는 generated SQL은 Advanced에서 열람한다.
## Advanced로 둘 내용
- routing payload
- tool allowlist
- generated SQL
- raw trace
## 우선순위
P2. MCP 진단용 화면으로서 기본 사용자보다 적용 담당자 중심으로 정리한다.

View File

@@ -0,0 +1,28 @@
# MCP 서비스 페이지 리뷰
- URL: `/mcp-sse`
- 현재 목적: MCP endpoint와 JSON-RPC 호출 정보를 제공한다.
- VPD/ASO 관련성: MCP 외부 클라이언트가 VPD 적용 Select AI tool을 호출하는 진입점이다.
## 페르소나 의견
- 일반 사용자: 프로토콜 설명보다 “어디에 등록하면 되는가”가 먼저 필요하다.
- 운영 관리자: endpoint, 인증 방식, 허용 tool, 상태를 한눈에 봐야 한다.
- 적용 담당자: Bearer token을 MCP 서버 인증과 업무 사용자 token으로 나누면 혼동된다. 가능하면 업무 토큰 하나로 설명해야 한다.
- DB 관리자: MCP 호출이 ORDS와 DB context 세팅을 거치는지 확인해야 한다.
## 가벼운 개선
1. 상단에 복사 가능한 client 설정 명세를 제공한다.
2. 인증 헤더는 실제 설계 기준으로 하나만 설명한다. 이중 토큰이 필요하면 이유를 명확히 쓴다.
3. 허용 tool이 하나라면 tool allowlist를 단순하게 표시한다.
## Advanced로 둘 내용
- JSON-RPC 예시
- streaming/http transport 차이
- timeout/retry 정책
## 우선순위
P2. 외부 연동 개발자에게 필요한 문서형 화면으로 정리한다.

View File

@@ -0,0 +1,29 @@
# 연동 점검 페이지 리뷰
- URL: `/mcp-client-demo`
- 현재 목적: Java client로 MCP 호출을 점검한다.
- VPD/ASO 관련성: MCP를 통해 호출한 정형 DB 결과에도 VPD/ASO가 적용되는지 확인한다.
## 페르소나 의견
- 일반 사용자: Java client 호출 정보는 과하다.
- 운영 관리자: 현재 endpoint가 호출 가능한지, 권한 오류인지, timeout인지 알고 싶다.
- 적용 담당자: client 설정과 서버 tool 명세가 일치하는지 확인해야 한다.
- DB 관리자: 호출이 어떤 DB profile과 ORDS endpoint를 쓰는지 추적하고 싶다.
## 가벼운 개선
1. 기본은 `연결 가능`, `도구 호출 가능`, `VPD 적용 결과 수신` 3개 상태로 표시한다.
2. curl 예시와 MCP client 설정 예시를 복사 버튼으로 제공한다.
3. Java stack/detail은 Advanced로 보낸다.
## Advanced로 둘 내용
- Java client raw request/response
- timeout 설정
- retry 횟수
- tool schema
## 우선순위
P2. 연동 개발자용 진단 화면이다.

View File

@@ -0,0 +1,29 @@
# 운영 현황 페이지 리뷰
- URL: `/operation-status`
- 현재 목적: 백오피스와 DB/ORDS/MCP 관련 운영 상태를 확인한다.
- VPD/ASO 관련성: 설정 상태와 DB 실제 적용 상태, 최근 동기화 결과를 구분해야 한다.
## 페르소나 의견
- 일반 사용자: 정상/주의/장애만 먼저 보고 싶다.
- 운영 관리자: 장애가 어느 기능에 영향을 주는지 알아야 한다.
- 적용 담당자: ASO 동기화 실패, VPD 정책 누락, ORDS 오류를 구분해야 한다.
- DB 관리자: DB 조회 기반 상태와 애플리케이션 설정 기반 상태를 분리해서 봐야 한다.
## 가벼운 개선
1. 상단에 전체 상태 배너를 둔다.
2. 상태 항목을 `앱`, `DB 연결`, `VPD 정책`, `ASO 정책`, `ORDS`, `MCP/Select AI`로 나눈다.
3. 각 항목에 최근 확인 시각, 영향 범위, 권장 조치를 표시한다.
## Advanced로 둘 내용
- raw health response
- SQL check query
- systemd/log 위치
- DB 오류 전문
## 우선순위
P0. 운영자가 “지금 정상인가”를 판단하는 중심 화면이다.

View File

@@ -0,0 +1,29 @@
# VPD 필터 구조 페이지 리뷰
- URL: `/vpd-filter-runtime`
- 현재 목적: `CB_AGENT_DOC_VPD_FILTER`의 연결 상태와 배포된 함수 소스를 읽기 전용으로 보여준다.
- VPD/ASO 관련성: VPD 행 필터가 권한 설정을 실제 WHERE predicate로 바꾸는 핵심 구조를 설명한다.
## 페르소나 의견
- 일반 사용자: 함수 소스는 기본적으로 너무 어렵다.
- 운영 관리자: 이 화면은 수정 화면이 아니라 근거 확인 화면이라는 점이 필요하다.
- 적용 담당자: 토큰이 context로 바뀌고, 필터가 context와 권한 테이블을 읽는 흐름을 이해해야 한다.
- DB 관리자: 실제 DB 함수 소스와 Git source가 일치하는지 확인하고 싶다.
## 가벼운 개선
1. 상단 도움말에 “토큰 → context → 권한 테이블 → VPD predicate → 결과 행” 흐름을 유지한다.
2. 함수 소스는 기본 접힘으로 두고, 주요 블록별 설명을 먼저 보여준다.
3. ASO와의 차이 표를 유지하되 “VPD는 컬럼 값을 NULL 처리하지 않는다”를 명시한다.
## Advanced로 둘 내용
- 전체 PL/SQL source
- safe column/static SQL 검증
- ALLOW/DENY predicate 조립
- policy binding metadata
## 우선순위
P1. 적용 담당자와 DB 관리자의 신뢰 확보용 화면이다.

View File

@@ -0,0 +1,29 @@
# DB 메타데이터 페이지 리뷰
- URL: `/schema-metadata`
- 현재 목적: 테이블/컬럼 comment와 annotation을 확인하고 수정한다.
- VPD/ASO 관련성: Select AI가 자연어 질의를 SQL로 바꿀 때 테이블·컬럼 의미를 이해하게 돕는 화면이다.
## 페르소나 의견
- 일반 사용자: comment와 annotation의 차이를 알기 어렵다.
- 운영 관리자: 자연어 질의 실패 원인이 메타데이터 부족일 수 있음을 알아야 한다.
- 적용 담당자: 업무 용어, 조인 키, 권한 기준 컬럼을 명확히 입력해야 한다.
- DB 관리자: 실제 DB comment와 백오피스 annotation 저장소가 어떻게 동기화되는지 봐야 한다.
## 가벼운 개선
1. 각 컬럼에 `업무 의미`, `권한 기준`, `Select AI 힌트`를 나눠 표시한다.
2. `CUST_ID`, `CONTRACT_NO`, `FC_ID`, `FC_CHANNEL` 같은 권한 기준 컬럼은 배지로 강조한다.
3. 자연어 질의 오류 리포트에서 이 화면으로 연결한다.
## Advanced로 둘 내용
- DB comment DDL
- annotation storage schema
- Select AI showprompt
- generated SQL 비교
## 우선순위
P1. 자연어 기반 Select AI 품질 개선의 핵심 운영 화면이다.

View File

@@ -0,0 +1,29 @@
# 보안 SQL 스크립트 페이지 리뷰
- URL: `/security-sql-scripts`
- 현재 목적: Git에 저장된 VPD/ASO/ORDS/Select AI 관련 SQL 스크립트를 확인하고 LLM 설명을 생성한다.
- VPD/ASO 관련성: 운영자가 실제 DB 적용 스크립트를 이해하고 검토하는 증적 화면이다.
## 페르소나 의견
- 일반 사용자: 전체 SQL보다 “이 스크립트가 뭘 적용하는가”가 먼저 필요하다.
- 운영 관리자: 실행 대상, 영향 범위, 되돌리기 가능 여부가 중요하다.
- 적용 담당자: 토큰/context/VPD/ASO 흐름을 스크립트 단위로 설명해야 한다.
- DB 관리자: 원본 SQL, 주석, LLM 설명, DB 적용 상태를 나란히 보고 싶다.
## 가벼운 개선
1. 스크립트마다 `목적`, `적용 객체`, `변경되는 DB 정책`, `검증 방법`을 상단 카드로 요약한다.
2. LLM 설명은 “토큰이 어떻게 context가 되고, VPD/ASO가 무엇을 읽는가”를 반드시 포함하게 한다.
3. 원본 SQL은 그대로 보여주되, 비전문가 설명은 먼저 제공한다.
## Advanced로 둘 내용
- 전체 SQL source
- script diff
- DB 배포 이력
- LLM prompt 전문
## 우선순위
P1. 복잡한 스크립트를 이해하기 위한 설명 화면으로 방향이 맞다.

View File

@@ -0,0 +1,29 @@
# 고급 접근 조건 페이지 리뷰
- URL: `/vpd-filter-policies`
- 현재 목적: 기본 접근 규칙으로 처리하기 어려운 고급 필터 정책을 관리한다.
- VPD/ASO 관련성: VPD predicate 생성에 영향을 주는 예외적 고급 조건이다.
## 페르소나 의견
- 일반 사용자: 이 화면은 기본 사용자가 접근할 이유가 없다.
- 운영 관리자: 대부분의 변경은 접근 규칙에서 끝난다는 안내가 현재 방향과 맞다.
- 적용 담당자: 고급 조건을 쓰기 전 업무 조건으로 해결 가능한지 판단해야 한다.
- DB 관리자: 임의 SQL 조건은 injection 방어와 검증 결과가 필요하다.
## 가벼운 개선
1. 기본 화면은 “접근 규칙으로 처리 가능한가?” 체크리스트부터 보여준다.
2. 고급 조건 작성은 Advanced로 접고, 저장 전 검증 결과를 필수로 표시한다.
3. 실제 predicate 예시와 실패 시 fail-closed 동작을 설명한다.
## Advanced로 둘 내용
- raw predicate
- validation rules
- 대상 컬럼 whitelist
- DBMS_ASSERT 처리
## 우선순위
P1. 잘못 쓰면 접근 범위를 넓힐 수 있으므로 일반 설정과 분리해야 한다.

View File

@@ -0,0 +1,32 @@
# 시스템 설정 페이지 리뷰
- URL: `/settings`
- 현재 목적: ORDS Base URL 등 시스템 연결 설정을 관리한다.
- VPD/ASO 관련성: 권한 검증과 MCP/ORDS 호출의 기반 연결 정보다.
## 페르소나 의견
- 일반 사용자: 일반 사용자가 자주 볼 화면은 아니다.
- 운영 관리자: 연결 URL 변경이 어떤 기능에 영향을 주는지 알아야 한다.
- 적용 담당자: DB 준비 상태와 ORDS 연결 설정을 구분해야 한다.
- DB 관리자: 운영 설정 변경 이력과 검증 결과가 필요하다.
## 가벼운 개선
1. 변경 가능한 설정과 읽기 전용 환경 설정을 분리한다.
2. 설정 변경 후 영향을 받는 기능을 표시한다.
- 접근 검증
- MCP 호출
- ORDS handler 조회
3. 저장 후 자동 health check를 실행하고 결과를 보여준다.
## Advanced로 둘 내용
- 환경 변수명
- systemd env 파일
- connection timeout
- proxy/TLS 설정
## 우선순위
P2. 기능 안정성에는 중요하지만 일반 권한 흐름보다는 후순위다.

View File

@@ -0,0 +1,29 @@
# DB 준비 상태 페이지 리뷰
- URL: `/settings/database`
- 현재 목적: 지원 테이블과 VPD runtime 준비 상태를 확인하고 초기 구성 SQL을 실행한다.
- VPD/ASO 관련성: 권한 운영에 필요한 메타 테이블, context package, policy function, ASO metadata의 준비 상태를 다룬다.
## 페르소나 의견
- 일반 사용자: 거의 볼 필요가 없는 관리자 화면이다.
- 운영 관리자: 운영 중 재실행하면 위험한 작업과 안전한 확인 작업을 구분해야 한다.
- 적용 담당자: 어떤 단계가 누락되면 어떤 화면이 실패하는지 알고 싶다.
- DB 관리자: 실행 전 SQL, 실행 권한, 생성 객체, 재실행 안전성이 필요하다.
## 가벼운 개선
1. 상태 확인과 변경 실행을 완전히 분리한다.
2. 변경 실행 전에는 생성/수정/건너뜀/위험 항목을 요약한다.
3. VPD와 ASO 준비 항목을 별도 섹션으로 표시한다.
## Advanced로 둘 내용
- 전체 DDL
- package/function source
- DB 권한 grant
- 재실행 idempotency 설명
## 우선순위
P1. DB 준비 화면은 강력한 변경 화면이므로 안전 장치와 설명이 필요하다.

View File

@@ -0,0 +1,51 @@
<!-- 함수별 설계서 템플릿. 복잡 함수마다 design/<issue-id>-<slug>/fn-<function_name>.md 로 작성.
작성: [AI] Architect, 구현 전 필수. -->
# 함수 설계서: `<function_name>` (#<issue-id>)
> **부모 설계서**: ./README.md · **상태**: Draft <!-- Draft|Approved|Superseded -->
> **작성**: [AI] Architect · **구현**: <file:function 또는 TBD> · **테스트**: <경로 또는 TBD>
## 1. 시그니처
```
<returnType> <function_name>(<params>) # 언어 확정 후 정확히 기재
```
## 2. 책임 (단일 책임, 1줄)
이 함수가 하는 단 하나의 일.
## 3. 입력
| 파라미터 | 타입 | 제약/검증 | 설명 |
|----------|------|-----------|------|
| `<p>` | | | |
## 4. 출력
- **반환**: 타입 / 의미.
- **부수효과**: (있으면 — I/O·상태변경 명시) / 없으면 **순수 함수**.
## 5. 동작 / 알고리즘
1. ...
2. ...
## 6. 에러 & 실패 모드
| 조건 | 처리 | 반환/예외 |
|------|------|-----------|
| | | |
## 7. 엣지케이스
- 경계값(0, 음수, 빈값, 최대), 동시성, 부분 실패.
## 8. 복잡도 / 성능
- 시간/공간 복잡도. 호출 빈도(예: 시세 폴링 루프 내부인가?).
## 9. 의존성
- 호출하는 함수/모듈, 외부 API, 설정 키.
## 10. 테스트 케이스
- [ ] 정상: <입력 → 기대 출력>
- [ ] 경계: ...
- [ ] 실패: ...
## 11. 추적성
- 인수조건: #<issue-id> 의 "<항목>".
- 관련 ADR: <ADR-NNNN 또는 없음>.

66
docs/design/_TEMPLATE.md Normal file
View File

@@ -0,0 +1,66 @@
<!-- 기능 설계서 템플릿. 복사해서 design/<issue-id>-<slug>/README.md 로 작성.
작성: [AI] Architect, 구현 전 필수. 빈 섹션 금지 — 해당 없으면 "해당 없음" 명시. -->
# 설계서: <기능명> (#<issue-id>)
> **상태**: Draft <!-- Draft | Approved | Superseded -->
> **작성**: [AI] Architect · **최종수정**: <YYYY-MM-DD>
> **추적성** — Redmine: #<issue-id> · 관련 ADR: <ADR-NNNN 또는 없음>
> · 구현 파일: <경로 또는 TBD> · 테스트: <경로 또는 TBD>
## 1. 목적 (Why)
이 기능이 푸는 문제. Planner 의 목표 1줄 인용.
## 2. 범위 (Scope)
- **포함**: ...
- **제외 (out of scope)**: ...
## 3. 인수조건 (Acceptance Criteria)
<!-- Planner 가 확정한 검증 가능한 항목. QA 가 이걸로 판정한다. -->
- [ ] ...
- [ ] ...
## 4. 컨텍스트 & 제약
- 의존성: 거래소 API / DB / 알림 / 외부 라이브러리.
- 제약: 성능, 레이트리밋, 리스크(돈), 보안.
- 가정: ...
## 5. 아키텍처 개요
- 모듈/파일 구조 (목록).
- 데이터 흐름 (텍스트 다이어그램).
- **I/O ↔ 순수 전략 로직 경계** 명시 (테스트 가능성).
```
<여기에 ASCII 흐름도>
```
## 6. 데이터 모델
- 입력 / 출력 / 저장 구조, 타입, **경계 검증 규칙**.
## 7. 함수 명세 (Function Specs)
<!-- 모든 함수를 나열. "복잡?" = 복잡이면 fn-<name>.md 개별 설계서 필수. -->
| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? |
|------|-----------|----------------|------|------|-----------|-------|
| `<name>` | | | | | | 단순 / **복잡** |
> 복잡 기준: 분기/상태기계, 외부 I/O, 리스크(주문·잔고) 경로, 비자명 알고리즘.
> → 해당 함수는 `fn-<name>.md` 작성. 단순(게터·포매터 등)은 이 표로 충분.
## 8. 흐름 / 알고리즘
- 핵심 시나리오 단계별. 상태 전이.
## 9. 엣지케이스 & 에러 처리
- 경계값, 실패 모드, 재시도/백오프.
- **안전한 기본값**(API 실패 시 거래 중단 등).
## 10. 테스트 계획
- 단위/통합 케이스 목록 (각 인수조건에 매핑).
- 모킹/드라이런 전략 (거래소 API 등).
## 11. 리스크 & 대안 검토
- 선택한 접근 vs 대안, 트레이드오프.
- 되돌리기 어려운 결정 → **ADR 로 분리** (`adr/NNNN-*.md`).
## 12. 미해결 질문 (Open Questions)
- ...

BIN
docs/ords_vpd_dds.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,42 @@
# Queue Protocol — 모든 페르소나 공통 규약
작업 큐 = Redmine 이슈. 각 페르소나는 자기 단계 이슈를 처리하고 git/Redmine 에 남긴 뒤 다음으로 넘긴다.
## 0. 환경 로드
```bash
set -a; . ./.env; set +a
RK="$REDMINE_API_KEY"; RB="$REDMINE_URL"; PROJ="$REDMINE_PROJECT"
# 카테고리 id 는 이름으로 조회(프로젝트마다 id 다름):
catid(){ curl -s -H "X-Redmine-API-Key: $RK" "$RB/projects/$PROJ/issue_categories.json" \
| python3 -c "import sys,json;[print(c['id']) for c in json.load(sys.stdin)['issue_categories'] if c['name']=='$1']"; }
```
## 1. 큐 매핑
- 현재 단계 = 카테고리 `01-Planner``08-Documenter`,`09-Done`.
- 수명주기 = 상태 신규(대기)/진행/완료/거절.
## 2. 내 작업 꺼내기
```bash
DEV=$(catid 03-Developer)
curl -s -H "X-Redmine-API-Key: $RK" "$RB/issues.json?project_id=$PROJ&category_id=$DEV&status_id=1&sort=id:asc&limit=1"
# 시작 시 상태 진행(2):
curl -s -H "X-Redmine-API-Key: $RK" -H "Content-Type: application/json" -X PUT "$RB/issues/<ID>.json" -d '{"issue":{"status_id":2}}'
```
## 3~4. 결과 남기기 (필수 3가지)
- (a) git 커밋+push (`[<Persona>] #<ID> ...`)
- (b) Redmine 저널 노트(역할 태그)
- (c) 다음 단계 전진: 카테고리=다음이름의 id, 상태 신규(1)
```bash
NEXT=$(catid 04-QA)
curl -s -H "X-Redmine-API-Key: $RK" -H "Content-Type: application/json" -X PUT "$RB/issues/<ID>.json" \
-d "{\"issue\":{\"category_id\":$NEXT,\"status_id\":1,\"notes\":\"[<Persona>] ...\"}}"
```
## 5. 게이트 반려
- QA(04)/Reviewer(06) 실패 → `03-Developer`. Developer 설계서 누락 → `02-Architect`. 사유를 노트에.
## 6. 종료 (Documenter)
- `09-Done` + 상태 완료(5) + done_ratio 100.
원칙: 자기 역할 범위만, 모든 변경 git 추적, 비밀(.env) 노출 금지.

View File

@@ -0,0 +1,108 @@
# 기존-01 MCP/RAG 진단 리포트
## 대상 시나리오
- 질문 번호: 기존-01
- 분류: 권한 + RAG 질문형
- 이해관계자: 설계사 `FC00789`
- 질의: `C1001006 고객 자동차보험 갱신 상담 전에, 현재 KB 계약(41048)과 삼성화재 보유 자동차보험 약관을 비교해서 고객에게 설명할 차별 포인트를 정리해줘.`
## 정형 MCP 확인 결과
정형 MCP 경로는 복구 확인됐다.
- MCP endpoint: `https://kb.cloud-handson.com/mcp`
- tool: `ords.query.kb_select_ai_vpd`
- Select AI profile: `KB_AIDP_SELECTAI_GPT55_OCI_PROFILE_V2`
- 검증 결과:
- `HTTP_STATUS=200`
- `MCP_HAS_ERROR=FALSE`
- `MCP_IS_ERROR=FALSE`
- `ITEM_COUNT=1`
생성 SQL은 `41048``CONTRACT_NO`가 아니라 `PRODUCT_CD`로 해석했다.
```text
KC.CUST_ID = 'C1001006'
KC.PRODUCT_CD = '41048'
EH.EXT_INSURER = '삼성화재'
EH.EXT_PRODUCT_GRP = '자동차'
```
대표 반환값:
```text
CUSTOMER_ID=C1001006
KB_CONTRACT_NO=CT2699001
KB_PRODUCT_CODE=41048
KB_PRODUCT_NAME=41048_KB개인용자동차보험
KB_PREMIUM=1350000
EXTERNAL_INSURER=삼성화재
EXTERNAL_PRODUCT_GROUP=자동차
EXTERNAL_PRODUCT_TYPE=개인용
EXTERNAL_CLAUSE_NAME=개인용애니카다이렉트자동차보험
```
## 적용한 정형 MCP 보완
- `POC_2` KB 업무 테이블/컬럼 comment를 보강했다.
- Select AI `showprompt` 확인 결과, comment와 annotation이 실제 모델 프롬프트에 포함됨을 확인했다.
- SQL 정규화/검증 로직을 보완했다.
- 마크다운 fence와 선행 wrapper comment를 제거한다.
- 문자열 리터럴 내부의 세미콜론/금지어를 실행 문법으로 오판하지 않도록 검사한다.
- DDL/DML/PLSQL/system object 차단은 유지한다.
변경 스크립트:
- `sql/adb/65_kb_select_ai_vpd_query_api.sql`
## RAG 근거 검색 확인 결과
RAG corpus는 `POC_2` 스키마에 존재한다.
```text
POC_2.KB_OWN_TERMS_DOCUMENTS
POC_2.KB_OWN_TERMS_CHUNKS
POC_2.KB_COMPETITOR_TERMS_DOCUMENTS
POC_2.KB_COMPETITOR_TERMS_CHUNKS
POC_2.KB_TERMS_CHUNKS_ALL_V
```
건수:
```text
TOTAL_CHUNKS=38772
OWN_CHUNKS=28506
COMP_CHUNKS=10266
```
KB 41048 약관 근거는 존재한다.
```text
DOC_41048|KB손해보험|OWN||KB개인용자동차보험|b5b680222361c4cde7a54855925333a3
SAMPLE_41048|KB손해보험|OWN||KB개인용자동차보험|...|자동차26-41048-1-04 KB개인용자동차보험 ...
```
## RAG 측 원인 판단
KB 41048 약관 chunk는 있지만 `PRODUCT_CODE` 메타데이터가 비어 있다.
```text
company_name=KB손해보험
company_type=OWN
product_code=<empty>
product_name=KB개인용자동차보험
chunk_text contains 자동차26-41048-1-04
```
따라서 RAG 검색/필터가 `PRODUCT_CODE = '41048'` 또는 상품코드 기반 필터를 사용하면 KB 근거가 제외될 수 있다. 본문 텍스트에는 `41048`이 있으므로 순수 텍스트 검색으로는 찾을 수 있지만, 메타데이터 필터 기준 검색에서는 빠질 가능성이 높다.
## 권고
RAG/약관 적재 담당 영역에서 아래 중 하나를 처리해야 한다.
1. `KB_OWN_TERMS_DOCUMENTS` / `KB_OWN_TERMS_CHUNKS` 적재 시 `자동차26-41048-1-04`에서 업무 상품코드 `41048`을 추출해 `PRODUCT_CODE`에 저장한다.
2. 기존 적재분에 대해 `KB개인용자동차보험` 또는 `자동차26-41048-1-04` 문서를 대상으로 `PRODUCT_CODE='41048'` backfill을 수행한다.
3. 검색 필터가 상품코드만 보지 말고 `PRODUCT_NAME`, `CHUNK_TEXT`의 약관 승인번호 패턴도 fallback으로 보도록 보강한다.
VPD 관리 인스턴스에서는 이 영역을 직접 수정하지 않고, 정형 데이터/Select AI 쪽 table comment와 annotation 관리 기능으로 메타데이터 품질을 운영 가능하게 한다.

View File

@@ -0,0 +1,319 @@
# Select AI 프로파일 전환 및 호출 시간 리포트
## 대상
- 날짜: 2026-07-10
- 대상 서비스: VPD 관리 인스턴스 MCP / ORDS Select AI 조회
- MCP endpoint: `https://kb.cloud-handson.com/mcp`
- MCP tool: `ords.query.kb_select_ai_vpd`
- ORDS endpoint: `/ords/cb-ords/kb-select-ai-vpd/query`
- 테스트 질의:
```text
C1001006 고객 자동차보험 갱신 상담 전에, 현재 KB 계약(41048)과 삼성화재 보유 자동차보험 약관을 비교해서 고객에게 설명할 차별 포인트를 정리해줘.
```
## 결론
기존 `openai.gpt-5.5` 기반 Select AI 프로파일은 SQL 생성 시간이 길고, 같은 질의에서도 SQL 생성 실패/거절 응답이 간헐적으로 발생했다.
최종 적용 프로파일은 아래로 변경했다.
```text
KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1
```
이 프로파일은 `openai.gpt-5.4-mini`를 사용하고, `comments=true`를 켜서 테이블/컬럼 comment 기반 SQL 생성을 유지한다. 최종 ORDS 직접 호출은 약 4.1초, MCP 호출은 약 4.6~8.2초 범위로 확인됐다.
## 기존 호출 시간
기존 운영 프로파일:
```text
KB_AIDP_SELECTAI_GPT55_OCI_PROFILE_V2
model=openai.gpt-5.5
provider=oci
region=us-chicago-1
provider_endpoint=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
comments=true
annotations=true
constraints=true
conversation=true
enforce_object_list=true
```
관찰된 기존 병목:
| 측정 구간 | 시간 | 결과 |
|---|---:|---|
| VPD context 설정 | 282ms | 정상 |
| 확정 SQL + VPD 실행 | 50ms | 정상 |
| `showprompt` metadata/prompt 생성 | 43.817초 | prompt 18,806자 |
| `showsql` 1차 | 38.770초 | SQL 생성 성공 |
| `showsql` 2차 | 105.505초 | non-SQL refusal |
| SQLcl direct package 호출 | 61.123초 | SQL 생성 및 2건 반환 |
| MCP 경로 호출 | 54.3초 | `ORA-20813`, Select AI가 유효 SELECT 생성 실패 |
| GPT-5.5 재비교 호출 | 72초 | SQL 대신 refusal 메시지 반환 |
판단:
- DB/VPD/ORDS 자체가 느린 것이 아니다.
- 주 병목은 `DBMS_CLOUD_AI.GENERATE` 내부의 Select AI prompt 구성과 LLM SQL 생성 단계다.
- `comments`, `annotations`, `constraints`, `conversation`, `enforce_object_list`가 모두 켜진 GPT-5.5 프로파일은 prompt가 커지고 응답 편차가 컸다.
## 후보 프로파일 테스트 결과
### 1. 단순 모델 교체 후보
| 프로파일 | 모델 | 설정 | 결과 |
|---|---|---|---|
| `KB_AIDP_SELECTAI_GPT5_MINI_PROFILE_V1` | `openai.gpt-5-mini` | 기존 GPT-5.5와 유사 | `ORA-20404 ... /actions/chat` |
| `KB_AIDP_SELECTAI_GPT41_MINI_PROFILE_V1` | `openai.gpt-4.1-mini` | 기존 GPT-5.5와 유사 | `ORA-20404 ... /actions/chat` |
| `KB_AIDP_SELECTAI_GROK43_PROFILE_V1` | `xai.grok-4.3` | 기존 GPT-5.5와 유사 | `ORA-20404 ... /actions/chat` |
| `KB_AIDP_SELECTAI_GPT54_MINI_PROFILE_V1` | `openai.gpt-5.4-mini` | 기존 GPT-5.5와 유사 | `ORA-20404 ... /actions/chat` |
오류:
```text
ORA-20404: Object not found - oci://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/chat
```
단순히 모델명만 바꾸는 방식은 안정적이지 않았다.
### 2. 기존 성공 프로파일 방식 확인
기존에 성공한 프로파일:
```text
POC_SELECT_AI_ALL
model=xai.grok-4.3
```
확인된 차이:
- `oci_compartment_id`가 있음
- `comments`, `annotations`, `constraints`, `conversation`, `enforce_object_list` 플래그가 없음
비교 결과:
| 프로파일 | 시간 | 결과 |
|---|---:|---|
| `KB_AIDP_SELECTAI_GPT55_OCI_PROFILE_V2` | 72초 | refusal 메시지 반환 |
| `POC_SELECT_AI_ALL` | 16초 | SQL 생성 성공 |
### 3. FAST 프로파일 테스트
`POC_SELECT_AI_ALL` 구조를 기준으로 `oci_compartment_id`를 포함하고, 무거운 메타데이터 플래그를 제거한 FAST 후보를 만들었다.
| 프로파일 | 모델 | showprompt | prompt 크기 | showsql | 결과 |
|---|---|---:|---:|---:|---|
| `KB_AIDP_SELECTAI_GROK43_FAST_PROFILE_V1` | `xai.grok-4.3` | 3초 | 5,049자 | 7초 | 성공 |
| `KB_AIDP_SELECTAI_GPT54_MINI_FAST_PROFILE_V1` | `openai.gpt-5.4-mini` | 0초 | 5,049자 | 4초 | 성공 |
FAST 방식은 빠르지만, 테이블/컬럼 comment 활용이 약해진다. 따라서 최종 적용용으로는 `comments=true`만 추가한 절충형을 별도 테스트했다.
### 4. Full metadata 프로파일 테스트
`comments=true`에 더해 `annotations=true`, `constraints=true`까지 켠 후보를 추가 테스트했다.
프로파일:
```text
KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1
model=openai.gpt-5.4-mini
comments=true
annotations=true
constraints=true
enforce_object_list=true
```
DBMS_CLOUD_AI 단독 측정:
| 회차 | showprompt | prompt 크기 | showsql | 결과 |
|---:|---:|---:|---:|---|
| 1 | 32초 | 19,816자 | 15초 | 성공 |
| 2 | 3초 | 19,816자 | 6초 | 성공 |
| 3 | 4초 | 19,816자 | 13초 | 성공 |
ORDS 실제 경로 측정:
| 회차 | HTTP | 총 시간 | 오류 | 반환 건수 |
|---:|---:|---:|---|---:|
| 1 | 200 | 8.224초 | 없음 | 2 |
| 2 | 200 | 7.288초 | 없음 | 2 |
| 3 | 200 | 6.217초 | 없음 | 2 |
평균:
```text
7.243초
```
비교:
| 구성 | ORDS 평균 | 상대 |
|---|---:|---:|
| `comments=true` only | 4.156초 | 1.0x |
| `comments + annotations + constraints` | 7.243초 | 약 1.7x 느림 |
판단:
- Full metadata 구성은 동작한다.
- 단, prompt 크기가 `12,520자`에서 `19,816자`로 증가한다.
- ORDS 기준 평균 응답 시간이 `4.156초`에서 `7.243초`로 늘어난다.
- 데모 응답성 기준으로는 `comments=true` only 구성이 더 적합하다.
## 최종 적용 프로파일
최종 적용:
```text
KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1
model=openai.gpt-5.4-mini
comments=true
enforce_object_list=true
oci_compartment_id=<configured>
```
테스트 결과:
| 구간 | 시간 | 결과 |
|---|---:|---|
| `showprompt` | 6초 | 성공 |
| prompt 크기 | 12,520자 | comment 포함 |
| `showsql` | 4초 | 성공 |
선택 이유:
- GPT-5.5 대비 훨씬 빠르다.
- FAST 프로파일보다 prompt가 크지만, 테이블/컬럼 comment를 유지한다.
- VPD 관리 인스턴스에서 보강한 table/column comment 운영 효과가 Select AI에 반영된다.
## 적용 내용
### DB 패키지
`POC_2.KB_SELECT_AI_VPD_QUERY_API`의 Select AI profile 상수를 변경했다.
```sql
c_profile_name CONSTANT VARCHAR2(128) := 'KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1';
```
대상 스크립트:
```text
sql/adb/65_kb_select_ai_vpd_query_api.sql
```
### ORDS 응답 표시
ORDS JSON 응답의 profile 표시도 새 프로파일로 변경했다.
```text
profile=KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1
```
대상 스크립트:
```text
sql/adb/66_kb_select_ai_vpd_query_ords.sql
```
### MCP 응답 표시
MCP tool 응답 payload의 profile 표시와 화면 설명을 새 프로파일 기준으로 변경했다.
대상 소스:
```text
src/main/java/com/cloudhandson/vpdbackoffice/service/McpSseService.java
src/main/resources/templates/mcp-sse.html
```
## 최종 속도 측정
### ORDS 직접 호출
호출 대상:
```text
https://g329127dfd380ad-kbaipoc.adb.ap-osaka-1.oraclecloudapps.com/ords/cb-ords/kb-select-ai-vpd/query
```
| 회차 | HTTP | 총 시간 | 응답 profile | 오류 | 반환 건수 |
|---:|---:|---:|---|---|---:|
| 1 | 200 | 4.058초 | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | 없음 | 2 |
| 2 | 200 | 4.216초 | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | 없음 | 2 |
| 3 | 200 | 4.195초 | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | 없음 | 1 |
평균:
```text
4.156초
```
### MCP 호출
호출 대상:
```text
https://kb.cloud-handson.com/mcp
```
| 회차 | HTTP | 총 시간 | MCP payload profile | ORDS profile | 오류 | 반환 건수 |
|---:|---:|---:|---|---|---|---:|
| 1 | 200 | 4.632초 | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | 없음 | 2 |
| 2 | 200 | 8.213초 | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | 없음 | 2 |
| 3 | 200 | 7.279초 | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | `KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1` | 없음 | 1 |
평균:
```text
6.708초
```
참고:
- MCP `time_starttransfer`는 약 0.008~0.012초였지만, 최종 응답 body 완료 기준은 `time_total`이다.
- MCP 경로는 백오피스 HTTP 처리와 ORDS 호출 wrapping이 추가되므로 ORDS 직접 호출보다 약간 느릴 수 있다.
## 개선 효과
| 비교 항목 | 기존 GPT-5.5 | 최종 GPT-5.4-mini comments |
|---|---:|---:|
| SQLcl direct package | 61.123초 | 4초대 SQL 생성 |
| 기존 MCP 실패 사례 | 54.3초 후 실패 | 4.6~8.2초 성공 |
| GPT-5.5 재비교 | 72초 후 refusal | 4.1초대 ORDS 성공 |
| prompt 크기 | 18,806~21,776자 | 12,520자 |
정리:
- 최종 ORDS 기준으로 기존 61~72초 구간 대비 약 10~17배 빠르다.
- MCP 기준으로도 기존 54.3초 실패 사례 대비 성공 응답이 약 4.6~8.2초로 줄었다.
- DB/VPD 실행 병목이 아니라 Select AI 프로파일/모델/메타데이터 구성이 핵심 병목이었다.
## 남은 주의점
1. 같은 자연어라도 Select AI 생성 SQL은 완전히 결정적이지 않다.
- 테스트에서도 반환 건수가 1~2건으로 흔들렸다.
- 이는 LLM SQL 생성 특성이다.
2. 비즈니스 품질을 더 안정화하려면 table/column comment와 annotation을 계속 보강해야 한다.
3. 특정 데모 질문은 deterministic SQL fallback 후보로 둘 수 있다.
- 예: `CUST_ID`, `PRODUCT_CD`, `EXT_INSURER`, `EXT_PRODUCT_GRP`가 명확한 비교 질문.
4. 장기적으로는 profile을 설정값으로 분리하는 것이 좋다.
- 현재는 DB package 상수로 고정되어 있다.
- 운영 전환 시 `CB_BACKOFFICE_SETTING` 또는 별도 Select AI 설정 테이블에서 profile을 읽도록 개선할 수 있다.
## 현재 권고
데모/PoC 운영 기준으로는 현재 적용한 아래 프로파일을 유지한다.
```text
KB_AIDP_SELECTAI_GPT54_MINI_COMMENTS_PROFILE_V1
```
이유:
- 응답 시간이 데모 가능한 수준이다.
- comment 기반 스키마 설명을 유지한다.
- ORDS/MCP 양쪽 모두 실제 호출 성공을 확인했다.

View File

@@ -0,0 +1,213 @@
# 데이터 접근 제어 백오피스 전체 UX/기능 리뷰
작성일: 2026-07-13
범위: 현재 Spring Boot 백오피스 화면, VPD/ASO/ORDS/MCP/Select AI 운영 흐름
상태: Review only
## 1. 결론
단순 문구 정리만으로는 충분하지 않다. 지금 화면은 기능은 많이 들어와 있지만, 사용자가 “업무 규칙을 넣으면 DB에서 어떻게 행/컬럼으로 적용되는가”를 끝까지 따라가기 어렵다.
가장 큰 개선 축은 세 가지다.
1. 전체 흐름을 작업 단위로 묶어야 한다.
- 사용자/역할 생성
- 행 접근 규칙 등록
- 보호 정책 연결
- 컬럼 마스킹 설정
- 토큰 발급
- 실제 접근 검증
2. 화면별 기본/고급을 분리해야 한다.
- 기본 화면: 업무 의미, 현재 상태, 다음 버튼, 검증 결과
- 고급 화면: VPD predicate, PL/SQL source, ORDS handler, raw JSON, SQL trace
3. 모든 설정 화면에서 “설정값 → DB 적용 → 실제 검증”이 이어져야 한다.
- 지금은 각 화면이 기능 단위로는 존재하지만, 다음 단계 연결과 검증 루프가 약하다.
## 2. 페르소나별 핵심 불만
| 페르소나 | 현재 불만 | 필요한 개선 |
|---|---|---|
| 일반 사용자 | VPD/ASO/ORDS/MCP가 섞여 무엇을 눌러야 할지 모른다 | “이 사용자는 무엇을 볼 수 있나?” 중심의 단순 경로 |
| 운영 관리자 | 변경 전후 영향과 실제 적용 여부를 한 번에 보기 어렵다 | 영향 사용자, 대상 객체, 검증 버튼, 최근 결과 |
| 적용 담당자 | 업무 조건이 WHERE predicate로 바뀌는 연결이 화면마다 끊긴다 | 업무 조건 → 저장 rule → VPD/ASO 해석 → 검증 결과 |
| DB 관리자 | 백오피스 설정과 DB 실제 정책이 일치하는지 증적이 부족하다 | DBMS_RLS/DBMS_REDACT/FGA/ORDS 상태를 한곳에서 확인 |
| 보안 담당자 | guest/read-only, 토큰 처리, 원문 표시 예외의 경계가 더 명확해야 한다 | 변경 가능 여부, 원문 표시 범위, 감사 증적 |
| 데모/영업 사용자 | KB 보험 시나리오가 화면 흐름으로 자연스럽게 보이지 않는다 | 설계사/지점장/관리자 시나리오 preset과 결과 비교 |
## 3. 우선순위 개선안
### P0. 반드시 해야 할 개선
#### 3.1 대시보드를 “전체 그림 + 오늘 할 일” 중심으로 재구성
현재 대시보드는 구조 설명은 좋아졌지만, 사용자가 다음 행동을 결정하기에는 아직 기능 나열에 가깝다.
개선:
- 상단에 3개 핵심 카드:
- 행 접근: “누가 어떤 행을 보는가”
- 컬럼 마스킹: “허용된 행의 어떤 컬럼을 원문/마스킹으로 보는가”
- 접근 검증: “토큰으로 실제 DB 결과를 확인한다”
- “처음 설정” 체크리스트:
1. 사용자/역할 준비
2. 행 접근 규칙 등록
3. 보호 상태 적용
4. 컬럼 마스킹 연결
5. 토큰 발급
6. 접근 검증
- DB 상태 요약:
- VPD 정책 누락 수
- ASO 정책 불일치 수
- ORDS handler 누락 수
- 최근 검증 실패 수
#### 3.2 접근 검증 결과 화면을 탭/단계형으로 재설계
접근 검증은 이 백오피스의 최종 판단 화면이다. 현재도 결과는 나오지만, “왜 그렇게 나왔는지”를 단계별로 보기에는 부족하다.
개선:
- 결과를 4개 섹션으로 분리:
1. 토큰 해석 결과: 사용자, stakeholder, role, channel
2. 행 접근 결과: 반환 행 수, 적용된 VPD predicate, ALLOW/DENY 근거
3. 컬럼 마스킹 결과: 마스킹 대상 컬럼, 원문 허용 여부, ASO policy 상태
4. DB 감사 증적: FGA SQL, RLS_INFO, request id
- 잘못된 토큰은 DB 오류처럼 보이지 않게:
- 토큰 없음
- 등록되지 않은 토큰
- 만료/회수 토큰
- 권한 없음
을 분리한다.
- 결과 테이블에서 마스킹된 컬럼에 아이콘/툴팁 표시.
#### 3.3 행 접근 규칙 화면에 “업무 조건 → predicate 변환”을 더 강하게 표시
현재도 wizard와 preview가 있으나, 적용 담당자가 원하는 것은 “내가 고른 업무 조건이 실제 어떤 WHERE 조각이 되는가”다.
개선:
- 조건 선택 옆에 즉시 preview:
- 본인 담당 계약 → `FC_ID = SYS_CONTEXT(...STAKEHOLDER_USER_ID...)`
- 채널 고객 → `EXISTS (...) FC_CHANNEL = SYS_CONTEXT(...STAKEHOLDER_CHANNEL...)`
- 정적 SQL 조건 → 검증된 현재 객체 컬럼 조건만 허용
- 목록 기본값은 raw rule보다 업무 문장 우선.
- 상세에는 저장 rule, 변환 predicate, 결합 방식(AND/OR/DENY)을 같이 표시.
- 삭제/변경 시 영향 사용자 수와 최근 검증 결과 링크 표시.
#### 3.4 컬럼 마스킹 화면에서 설정 단계와 DB 적용 상태를 더 선명하게 분리
지금 기능은 맞지만 사용자에게는 “대상 컬럼 추가”, “규칙 연결”, “사용자 원문 허용”, “DB 정책 동기화”가 섞여 보일 수 있다.
개선:
- 단계형 표시:
1. 마스킹 후보 컬럼 등록
2. 기본 마스킹 규칙 연결
3. 원문 표시 허용 사용자 지정
4. DB ASO 정책 동기화 상태 확인
- 컬럼별 “현재 실제 동작” preview:
- 일반 사용자: 마스킹
- 원문 허용 사용자: 원문
- 행 접근 권한 없는 사용자: 행 없음
- DBMS_REDACT 정책 상태와 백오피스 설정 차이를 컬럼 단위로 표시.
#### 3.5 운영 현황을 통합 health dashboard로 강화
현재 운영 현황은 표가 있지만, 장애 상황에서 “무엇이 문제인지”를 바로 알려주는 구조가 약하다.
개선:
- 상단 전체 상태:
- 정상 / 주의 / 장애
- 영역별 health:
- App 로그인/세션
- DB 연결
- VPD 정책
- ASO 정책
- ORDS handler
- MCP/Select AI endpoint
- 각 상태에:
- 마지막 확인 시각
- 영향받는 기능
- 다음 조치
- 관련 화면 이동 링크
## 4. 화면별 추가 리뷰
| 화면 | 현재 상태 | 남은 개선 |
|---|---|---|
| 로그인 | 제품명 정리됨 | 백오피스 계정과 업무 사용자 토큰이 다르다는 안내, guest/read-only 안내 보강 |
| 대시보드 | 전체 구조 설명 있음 | 실제 작업 체크리스트와 상태 요약 부족 |
| 사용자 | application user 설명 있음 | `KB_STAKEHOLDERS` 매핑 상태, 직접 역할/그룹 역할/토큰 발급 링크 부족 |
| 그룹 | 영향 사용자/역할 일부 표시 | 그룹이 지점/채널 조건이 아니라 역할 상속 단위라는 설명 보강 |
| 역할 | 삭제 영향 표시 있음 | 역할별 접근 규칙 수, 원문 허용 컬럼 수, 영향 사용자 요약 보강 |
| 행 접근 규칙 | wizard와 preview 있음 | 업무 조건별 predicate preview, 삭제 영향/검증 링크 보강 |
| 컬럼 원문 표시 허용 | VPD/ASO 경계 설명 있음 | 조건부 원문 허용이 아니라 사용자 단위 UNMASK라는 한계 명시 필요 |
| 사용자별 접근 확인 | 설정 기반 권한 확인 가능 | 실제 DB 결과가 아니라 예상 권한이라는 구분과 접근 검증 CTA 강화 |
| 보호 상태 | VPD 적용 상태 확인 가능 | `설정됨 / DB 적용됨 / 최근 검증 성공` 3단계 배지 필요 |
| 컬럼 마스킹 | ASO 동기화 기능 있음 | 컬럼별 실제 사용자 결과 preview와 정책 불일치 원인 설명 필요 |
| 검증 세션 | 토큰 발급/이력 있음 | “토큰은 권한을 담지 않고 사용자 식별만 한다”를 더 강하게 표시 |
| 접근 검증 | 핵심 검증 가능 | 결과를 토큰/context/행/컬럼/감사 증적으로 분리 필요 |
| 조회 대상 | ORDS 대상 등록 가능 | 신규 대상 등록 후 다음 단계 안내 부족 |
| 정형 데이터 조회 | 관리자 preview 가능 | 사용자별 접근 결과가 아님을 더 강하게 표시, 권한 기준 컬럼 배지 필요 |
| 조회 연동 | ORDS source 확인 가능 | 기본은 endpoint 상태, source는 Advanced로 더 숨기는 편이 좋음 |
| 지식 검색 | 등록/검색/정책이 한 화면 | 자료 등록, 접근 정책, 검색 검증을 탭으로 분리 필요 |
| 대화형 검색 | 자연어 질의 가능 | 결과를 답변/정형 결과/근거 문서/오류 원인으로 분리 필요 |
| 검색 해석 | 라우팅 결과 확인 가능 | 단계별 타임라인과 소요시간, showprompt/showsql Advanced 필요 |
| MCP 서비스 | endpoint/tool 설명 있음 | 복사 가능한 client 설정 명세와 curl 예시를 상단에 제공 |
| 연동 점검 | client 호출 가능 | 연결 가능/도구 호출 가능/권한 적용 결과 3단계 health 필요 |
| 운영 현황 | 정책/ORDS/ASO 상태 표 있음 | 통합 health, 최근 확인 시각, 영향 범위, 조치 링크 필요 |
| 행 접근 필터 구조 | 기술 흐름 있음 | 함수 소스보다 블록별 설명과 Git/DB source 차이 표시가 우선 |
| DB 메타데이터 | comment/annotation 수정 가능 | 권한 기준 컬럼 배지, Select AI 실패 리포트와 연결 필요 |
| 보안 SQL 스크립트 | 원문/LLM 설명 가능 | 스크립트별 목적/대상/변경 정책/검증 방법 summary card 필요 |
| 고급 접근 조건 | Advanced 성격 있음 | 기본 접근 규칙으로 해결 가능한지 체크리스트 선행 필요 |
| 시스템 설정 | ORDS Base URL 관리 | 저장 후 자동 health check와 영향 기능 표시 필요 |
| DB 준비 상태 | preflight와 DDL 있음 | 확인 작업과 변경 작업을 더 강하게 분리, 실행 전 영향 요약 필요 |
## 5. 설계상 더 명확히 해야 할 원칙
### 5.1 토큰은 권한 묶음이 아니다
토큰은 사용자를 식별하고 context를 세팅하는 열쇠다. 실제 권한은 요청 시점에 사용자/그룹/역할/행 접근 규칙/마스킹 규칙을 조회해서 계산된다.
화면 전반에 이 문장을 반복해야 한다.
### 5.2 VPD와 ASO의 결합 방식
- VPD는 행을 줄인다.
- ASO는 남은 행의 컬럼 표시 방식을 바꾼다.
- 원문 표시 허용은 행 접근 권한을 늘리지 않는다.
- 행 접근 권한이 없으면 ASO 원문 허용도 의미가 없다.
### 5.3 “집계만 허용”은 별도 설계가 필요하다
ASO로 마스킹된 컬럼에 대해 자연스럽게 집계가 된다고 가정하면 안 된다. 지점장에게 상세는 마스킹하고 집계만 허용하려면 trusted API, aggregate 전용 path, 또는 별도 검증 가능한 query boundary가 필요하다.
### 5.4 Select AI 품질은 메타데이터 운영 문제다
자연어 질의 실패를 프롬프트로만 해결하면 재현성이 떨어진다. 테이블/컬럼 comment, annotation, constraint, 업무명, 조인 키를 운영자가 보강하는 흐름이 있어야 한다.
## 6. 추천 구현 순서
### 1차: 운영 사고를 줄이는 P0
1. 접근 검증 결과 화면 재구성
2. 운영 현황 health dashboard 강화
3. 행 접근 규칙 predicate preview/영향도 강화
4. 컬럼 마스킹 단계형 구성과 사용자별 preview
### 2차: 온보딩과 이해도 개선
1. 대시보드 체크리스트와 상태 요약
2. 사용자/역할/그룹 영향도 보강
3. 보호 상태 3단계 배지
4. 조회 대상 등록 후 다음 단계 안내
### 3차: MCP/Select AI 품질과 고급 운영
1. DB 메타데이터 권한 기준 컬럼 배지
2. MCP/검색 결과 타임라인과 소요시간 표시
3. 보안 SQL 스크립트 summary card
4. 고급 접근 조건 체크리스트와 검증 강화

View File

@@ -2,12 +2,14 @@
## 운영 구조
- 공개 endpoint: `https://<소유 FQDN>` 또는 `https://<고정 public IPv4>`
- Caddy: VM의 80/443에서 TLS termination, HTTP redirect, HSTS, 인증서 자동 발급·갱신
- Spring Boot: `127.0.0.1:8082`에서만 수신
- 외부 `8082/tcp`: OCI NSG와 VM firewalld 모두 deny
- 공개 endpoint: `https://kb.cloud-handson.com`
- 공개 배포 VM: `opc@161.33.6.45`
- Nginx: VM의 80/443에서 TLS termination, HTTP redirect, HSTS
- Spring Boot: `127.0.0.1:8080`에서만 수신
- 외부 애플리케이션 포트 직접 접근: OCI NSG와 VM firewalld 모두 deny
- 인증서: Lets Encrypt / Certbot Nginx plugin
FQDN을 쓰면 A 레코드는 VM public IP를 가리켜야 한다. DNS가 없는 고정 public IPv4는 Lets Encrypt `shortlived` profile 기반 약 6일 인증서를 사용하며 Caddy 자동 갱신이 정상인지 반드시 모니터링한다. 임시 공개 DNS 서비스와 Caddy 로컬 CA 인증서는 운영 endpoint로 사용하지 않는다.
FQDN A 레코드는 VM public IP `161.33.6.45`를 가리켜야 한다. 현재 운영 경로는 Nginx가 `127.0.0.1:8080``vpd-backoffice.service`로 프록시하는 구조다. `hermes` 또는 `8082` 응답만 보고 운영 반영 완료로 판단하지 않는다.
## 최초 준비
@@ -16,15 +18,8 @@ FQDN을 쓰면 A 레코드는 VM public IP를 가리켜야 한다. DNS가 없는
1. 소유 FQDN의 A 레코드를 VM public IP로 설정하거나 고정 public IPv4 사용을 확정한다.
2. OCI NSG와 VM firewalld에서 80/443 ingress를 허용한다.
3. 기존 8082 ingress를 OCI NSG와 firewalld에서 제거한다.
4. Oracle Linux/RHEL 계열 VM에 공식 Caddy 패키지를 설치한다.
```bash
sudo dnf install -y dnf-plugins-core
sudo dnf copr enable -y @caddy/caddy
sudo dnf install -y caddy
```
공식 패키지는 `caddy.service``/etc/caddy/Caddyfile`을 제공한다. 설정 스크립트가 service enable/start를 처리한다.
4. Oracle Linux/RHEL 계열 VM에 Nginx와 Certbot Nginx plugin을 설치한다.
5. `vpd-backoffice.service``127.0.0.1:8080`에만 바인딩한다.
## 적용
@@ -32,78 +27,58 @@ sudo dnf install -y caddy
```bash
mvn test
scripts/test-backoffice-https-config.sh
scripts/deploy-backoffice-vm.sh --host hermes
```
HTTPS 설정을 dry-run으로 확인한 다음 적용한다.
운영 배포는 `161.33.6.45` 대상에 수행한다. SSH alias를 쓴다면 해당 alias가 반드시 `opc@161.33.6.45`를 가리키는지 먼저 확인한다.
```bash
scripts/configure-backoffice-https-vm.sh \
--host hermes \
--public-host admin.example.com \
--tls-email ops@example.com \
--expected-address 130.162.134.59 \
--dry-run
scripts/configure-backoffice-https-vm.sh \
--host hermes \
--public-host admin.example.com \
--tls-email ops@example.com \
--expected-address 130.162.134.59
ssh <운영-alias> 'hostname; hostname -I; systemctl status vpd-backoffice --no-pager'
```
실제 endpoint, 이메일, 주소로 바꿔 실행한다. FQDN 대신 IP를 쓰는 현재 hermes 예시는 `--public-host 130.162.134.59 --expected-address 130.162.134.59`다. 스크립트는 DNS/IP 일치, Caddy/sudo, Caddyfile 문법, service 상태를 확인하고 기존 설정을 `~/apps/vpd-backoffice/caddy-backups`에 저장한다.
배포 후에는 systemd 서비스를 재시작하고 공개 URL로 확인한다.
```bash
ssh <운영-alias> 'sudo systemctl restart vpd-backoffice'
curl -k -sS https://kb.cloud-handson.com/login
```
## 반복 검증
설정을 바꾸지 않고 외부 검증만 다시 수행할 수 있다.
```bash
scripts/configure-backoffice-https-vm.sh \
--host hermes \
--public-host admin.example.com \
--tls-email ops@example.com \
--expected-address 130.162.134.59 \
--verify-only
```
검증 항목:
- HTTP `/login`이 동일 host의 HTTPS로 전환됨
- HTTPS 인증서가 공개 신뢰됨
- HSTS 1년
- `JSESSIONID`의 Secure/HttpOnly/SameSite=Lax
- 외부 `:8082` 직접 연결 실패
- 외부 애플리케이션 포트 직접 연결 실패
- `https://kb.cloud-handson.com/login`의 HTML이 현재 배포된 jar의 로그인 화면과 일치
## 인증서 갱신과 모니터링
Caddy는 공개 DNS 이름의 인증서를 자동 갱신하며, 공식 systemd service의 인증서 상태는 `/var/lib/caddy/.local/share/caddy`에 유지된다. 별도 cron이나 certbot hook을 추가하지 않는다.
Certbot timer가 Lets Encrypt 인증서를 갱신한다. Nginx 설정과 인증서 갱신 상태를 함께 확인한다.
```bash
ssh hermes 'systemctl is-active caddy && systemctl is-enabled caddy'
ssh hermes 'sudo journalctl -u caddy --since "24 hours ago" --no-pager'
ssh hermes 'sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile'
ssh <운영-alias> 'systemctl is-active nginx && systemctl is-enabled nginx'
ssh <운영-alias> 'systemctl is-active certbot-renew.timer && systemctl is-enabled certbot-renew.timer'
ssh <운영-alias> 'sudo nginx -t'
ssh <운영-alias> 'sudo journalctl -u nginx --since "24 hours ago" --no-pager'
```
ACME 오류, 인증서 만료 경고, 반복 reload 실패를 알림 대상으로 삼는다. VM 백업에서 Caddy data directory와 앱의 Caddyfile backup을 함께 보존한다.
ACME 오류, 인증서 만료 경고, 반복 reload 실패를 알림 대상으로 삼는다.
## 장애와 롤백
1. 앱이 살아 있는지 VM 내부에서 확인한다.
```bash
ssh hermes 'curl -sS -H "X-Forwarded-Proto: https" -H "X-Forwarded-Host: admin.example.com" -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8082/login'
ssh <운영-alias> 'curl -sS -H "X-Forwarded-Proto: https" -H "X-Forwarded-Host: kb.cloud-handson.com" -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/login'
```
2. Caddy 로그와 설정을 확인한다.
3. 설정 변경 직후 장애라면 가장 최근 backup을 복원한다.
2. Nginx 로그와 설정을 확인한다.
3. 설정 변경 직후 장애라면 가장 최근 Nginx 설정 backup을 복원한다.
```bash
ssh hermes 'ls -1t ~/apps/vpd-backoffice/caddy-backups/Caddyfile.* | head -n 3'
ssh hermes 'sudo install -o root -g root -m 644 ~/apps/vpd-backoffice/caddy-backups/Caddyfile.<timestamp> /etc/caddy/Caddyfile && sudo systemctl reload caddy'
```
4. 앱 jar 롤백이 필요하면 직전 승인된 artifact를 배포하고 앱과 Nginx를 모두 재검증한다.
4. 앱 jar 롤백이 필요하면 직전 승인된 artifact를 배포하고 앱과 Caddy를 모두 재검증한다.
장애 우회를 위해 `8082`를 다시 공개하지 않는다. 서비스 중단이나 방화벽/NSG 롤백은 개별 승인을 받는다.
장애 우회를 위해 애플리케이션 포트를 직접 공개하지 않는다. 서비스 중단이나 방화벽/NSG 롤백은 개별 승인을 받는다.