Consolidate data access control backoffice updates
20
.env.example
@@ -26,11 +26,23 @@ export BACKOFFICE_DB_PASSWORD="${ADB_PASSWORD}"
|
||||
# --- (2b) Spring Boot 백오피스 ---
|
||||
export BACKOFFICE_ADMIN_USER="admin"
|
||||
export BACKOFFICE_ADMIN_PASSWORD="admin"
|
||||
# 운영에서는 최초 한 번 생성한 {bcrypt} 해시를 .env에 보관합니다. 이를 사용하면
|
||||
# 지속 로그인 쿠키가 앱 재기동 후에도 유효합니다. 원문 비밀번호 값은 Git에 넣지 마세요.
|
||||
export BACKOFFICE_ADMIN_PASSWORD_HASH=""
|
||||
# 읽기 전용 조회 계정. 활성화하면 ROLE_VIEWER로 로그인 가능하며 POST/PUT/PATCH/DELETE는 차단됩니다.
|
||||
export BACKOFFICE_GUEST_ENABLED="false"
|
||||
export BACKOFFICE_GUEST_USER="guest"
|
||||
export BACKOFFICE_GUEST_PASSWORD=""
|
||||
export BACKOFFICE_GUEST_PASSWORD_HASH=""
|
||||
# 로컬 HTTP 개발 기본값. 외부 VM 배포 스크립트는 loopback/HTTPS 안전값으로 덮어씁니다.
|
||||
export BACKOFFICE_BIND_ADDRESS="0.0.0.0"
|
||||
export BACKOFFICE_FORWARD_HEADERS_STRATEGY="none"
|
||||
export BACKOFFICE_REQUIRE_HTTPS="false"
|
||||
export BACKOFFICE_SESSION_COOKIE_SECURE="false"
|
||||
# HTTPS에서만 사용. key는 배포 환경의 secret으로 32자 이상 임의값을 넣고 Git에 저장하지 않습니다.
|
||||
export BACKOFFICE_REMEMBER_ME_ENABLED="false"
|
||||
export BACKOFFICE_REMEMBER_ME_KEY=""
|
||||
export BACKOFFICE_REMEMBER_ME_DAYS="14"
|
||||
export BACKOFFICE_ORDS_BASE_URL="https://yh0olybn5pqce4n-d8aukro81636mon0.adb.ap-seoul-1.oraclecloudapps.com/ords"
|
||||
export BACKOFFICE_ORDS_TIMEOUT_SECONDS="10"
|
||||
# ORDS metadata 생성/수정 전용 계정. 비워두면 BACKOFFICE_DB_* 연결을 사용하므로
|
||||
@@ -41,10 +53,18 @@ export BACKOFFICE_ORDS_DB_PASSWORD=""
|
||||
|
||||
# --- (2c) OpenAI 호환 AI 호출 (MCP-style Reasoning 탭) ---
|
||||
export BACKOFFICE_AI_ENABLED="false"
|
||||
export BACKOFFICE_AI_PROVIDER="openai" # openai | oci
|
||||
export BACKOFFICE_AI_BASE_URL="" # 예: https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
|
||||
export BACKOFFICE_AI_MODEL="" # 예: openai.gpt-5.4-mini
|
||||
export BACKOFFICE_AI_API_KEY=""
|
||||
export BACKOFFICE_AI_TIMEOUT_SECONDS="30"
|
||||
# provider=oci이면 API key를 환경 변수로 복제하지 않고 OCI config profile로 요청에
|
||||
# 서명합니다. POC3_LLM_GPT55_OCI_*/OCI_GENAI_*가 있으면 아래 BACKOFFICE_* 값보다
|
||||
# fallback으로 사용됩니다.
|
||||
export BACKOFFICE_AI_OCI_CONFIG_FILE=""
|
||||
export BACKOFFICE_AI_OCI_PROFILE="DEFAULT"
|
||||
export BACKOFFICE_AI_OCI_REGION=""
|
||||
export BACKOFFICE_AI_OCI_COMPARTMENT_ID=""
|
||||
|
||||
# 로컬 키 별칭은 .env에만 저장하세요. .env.example에는 원문 키를 넣지 않습니다.
|
||||
export VPDTEST1_API_KEY=""
|
||||
|
||||
1852
docs/06-agent-ords-vpd-dds-security-brief.md
Normal file
63
docs/README.md
Normal 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
@@ -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>** — 기각 사유: ...
|
||||
22
docs/assets/agent-ords-security-audit-screen.svg
Normal 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 |
22
docs/assets/agent-ords-security-bearer-key-screen.svg
Normal 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 |
72
docs/assets/agent-ords-security-dds-bearer-probe.svg
Normal 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 |
88
docs/assets/agent-ords-security-dds-object-model.svg
Normal 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 |
22
docs/assets/agent-ords-security-dds-screen.svg
Normal 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 |
97
docs/assets/agent-ords-security-dds-setup-flow.svg
Normal 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 |
85
docs/assets/agent-ords-security-main-trunk.svg
Normal 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 |
31
docs/assets/agent-ords-security-no-permission-screen.svg
Normal 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 <key></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 |
@@ -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 |
60
docs/assets/agent-ords-security-sys-context-flow.svg
Normal 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 |
105
docs/assets/agent-ords-security-two-scenarios.svg
Normal 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 |
22
docs/assets/agent-ords-security-vpd-screen.svg
Normal 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 |
73
docs/assets/agent-ords-security-vpd-where-flow.svg
Normal 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 |
@@ -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` |
|
||||
| 인증서 | Let’s 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` 연결이 성공한다.
|
||||
|
||||
200
docs/design/620-poc4-mcp-discovery-streamable-http/README.md
Normal 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`에 민감 데이터 보존 기간과 삭제 정책을 별도로 정해야 한다.
|
||||
83
docs/design/621-vpd-aso-persona-page-review/README.md
Normal 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용 메타데이터 화면은 자연어 질의 품질에 직접 영향을 주므로, 단순 주석 편집이 아니라 “이 컬럼이 어떤 업무 의미인지”를 사람이 이해하고 보강하는 화면으로 다뤄야 한다.
|
||||
@@ -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. 혼동을 줄이는 문구 개선이 중심이고, 권한 계산 로직에는 영향이 없다.
|
||||
@@ -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. 홈에서 전체 모델을 정확히 잡으면 다른 화면의 설명량을 줄일 수 있다.
|
||||
@@ -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. 권한 주체를 이해해야 접근 규칙과 토큰 검증의 혼동이 줄어든다.
|
||||
@@ -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. 운영 관리자가 대량 영향 변경을 안전하게 이해하는 데 필요하다.
|
||||
@@ -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. 접근 규칙의 주체가 역할이므로 사용자 영향도를 쉽게 보여줘야 한다.
|
||||
@@ -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로 바꾼다”는 표현을 반드시 강화해야 한다.
|
||||
@@ -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 권한을 섞어 이해하면 운영 사고로 이어질 수 있다.
|
||||
@@ -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. 운영자가 변경 전후 영향 확인에 사용하는 중심 화면으로 만들 필요가 있다.
|
||||
@@ -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에 적용됐는지”를 판단하는 핵심 운영 화면이다.
|
||||
@@ -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 적용 여부를 화면에서 바로 판단할 수 있어야 한다.
|
||||
@@ -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 설정 입력이라는 점을 명확히 해야 한다.
|
||||
@@ -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. 이 화면은 모든 설정 변경의 성공 기준이다.
|
||||
@@ -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. 신규 테이블 온보딩 흐름을 단순하게 만드는 것이 핵심이다.
|
||||
@@ -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 결과의 차이를 분명히 해야 한다.
|
||||
@@ -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. 운영 화면과 개발자 화면의 밀도를 분리해야 한다.
|
||||
@@ -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 질의 품질에는 중요하다.
|
||||
@@ -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. 데모 품질에는 중요하지만, 먼저 권한 운영 화면을 정리한 뒤 다루는 것이 좋다.
|
||||
@@ -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 진단용 화면으로서 기본 사용자보다 적용 담당자 중심으로 정리한다.
|
||||
@@ -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. 외부 연동 개발자에게 필요한 문서형 화면으로 정리한다.
|
||||
@@ -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. 연동 개발자용 진단 화면이다.
|
||||
@@ -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. 운영자가 “지금 정상인가”를 판단하는 중심 화면이다.
|
||||
@@ -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 관리자의 신뢰 확보용 화면이다.
|
||||
@@ -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 품질 개선의 핵심 운영 화면이다.
|
||||
@@ -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. 복잡한 스크립트를 이해하기 위한 설명 화면으로 방향이 맞다.
|
||||
@@ -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. 잘못 쓰면 접근 범위를 넓힐 수 있으므로 일반 설정과 분리해야 한다.
|
||||
@@ -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. 기능 안정성에는 중요하지만 일반 권한 흐름보다는 후순위다.
|
||||
@@ -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 준비 화면은 강력한 변경 화면이므로 안전 장치와 설명이 필요하다.
|
||||
51
docs/design/_FN_TEMPLATE.md
Normal 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
@@ -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
42
docs/pipeline/QUEUE-PROTOCOL.md
Normal 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) 노출 금지.
|
||||
108
docs/reports/2026-07-10-existing-01-mcp-rag-diagnosis.md
Normal 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 관리 기능으로 메타데이터 품질을 운영 가능하게 한다.
|
||||
319
docs/reports/2026-07-10-select-ai-profile-performance-report.md
Normal 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 양쪽 모두 실제 호출 성공을 확인했다.
|
||||
213
docs/reports/2026-07-13-vpd-aso-holistic-ui-review.md
Normal 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. 고급 접근 조건 체크리스트와 검증 강화
|
||||
@@ -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
|
||||
- 인증서: Let’s Encrypt / Certbot Nginx plugin
|
||||
|
||||
FQDN을 쓰면 A 레코드는 VM public IP를 가리켜야 한다. DNS가 없는 고정 public IPv4는 Let’s 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가 Let’s 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 롤백은 개별 승인을 받는다.
|
||||
|
||||
1
ops/sonarqube/99-sonarqube.conf
Normal file
@@ -0,0 +1 @@
|
||||
vm.max_map_count = 524288
|
||||
15
ops/sonarqube/sonarcube.cloud-handson.com.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name sonarcube.cloud-handson.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:9000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
}
|
||||
20
ops/sonarqube/sonarqube-community.service
Normal file
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=SonarQube Community Build (evaluation)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=opc
|
||||
Group=opc
|
||||
WorkingDirectory=/home/opc/tools/sonarqube-26.6.0.123539
|
||||
Environment=SONAR_WEB_HOST=127.0.0.1
|
||||
Environment=SONAR_WEB_PORT=9000
|
||||
ExecStart=/bin/bash /home/opc/tools/sonarqube-26.6.0.123539/bin/linux-x86-64/sonar.sh console
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStartSec=180
|
||||
LimitNOFILE=131072
|
||||
LimitNPROC=8192
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
43
pom.xml
@@ -19,10 +19,12 @@
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<mybatis-spring-boot.version>3.0.4</mybatis-spring-boot.version>
|
||||
<oracle.jdbc.version>23.6.0.24.10</oracle.jdbc.version>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<mybatis-spring-boot.version>3.0.4</mybatis-spring-boot.version>
|
||||
<oracle.jdbc.version>23.6.0.24.10</oracle.jdbc.version>
|
||||
<!-- GPT-5.5 response/schema support is supplied by the current OCI SDK. -->
|
||||
<oci.sdk.version>3.90.1</oci.sdk.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -57,6 +59,21 @@
|
||||
<artifactId>oraclepki</artifactId>
|
||||
<version>${oracle.jdbc.version}</version>
|
||||
</dependency>
|
||||
<!-- OCI API-key signing for the SQL-script explanation assistant. The
|
||||
private key remains in the configured OCI config file, never in the
|
||||
application environment or Git. -->
|
||||
<dependency>
|
||||
<groupId>com.oracle.oci.sdk</groupId>
|
||||
<artifactId>oci-java-sdk-generativeaiinference</artifactId>
|
||||
<version>${oci.sdk.version}</version>
|
||||
</dependency>
|
||||
<!-- OCI SDK service modules need exactly one concrete HTTP transport. The
|
||||
Jersey 3 transport is compatible with this Spring Boot 3 application. -->
|
||||
<dependency>
|
||||
<groupId>com.oracle.oci.sdk</groupId>
|
||||
<artifactId>oci-java-sdk-common-httpclient-jersey3</artifactId>
|
||||
<version>${oci.sdk.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -71,6 +88,24 @@
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
</resource>
|
||||
<!-- Keep the reviewed SQL in sql/adb as the sole Git source, and bundle
|
||||
only the curated read-only viewer set into the deployed JAR. -->
|
||||
<resource>
|
||||
<directory>sql/adb</directory>
|
||||
<targetPath>sql/adb</targetPath>
|
||||
<includes>
|
||||
<include>62_kb_aso_masking_backoffice_metadata.sql</include>
|
||||
<include>63_kb_aso_masking_rule_runtime.sql</include>
|
||||
<include>64_kb_aso_masking_default_column_rules.sql</include>
|
||||
<include>65_kb_select_ai_vpd_query_api.sql</include>
|
||||
<include>66_kb_select_ai_vpd_query_ords.sql</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
21
scripts/enqueue.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# 새 작업을 파이프라인 큐에 투입 = 01-Planner/신규 Redmine 이슈 생성.
|
||||
# 사용법: ./scripts/enqueue.sh "제목" ["요구사항"]
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
set -a; . ./.env; set +a
|
||||
SUBJECT="${1:?사용법: enqueue.sh \"제목\" [\"요구\"]}"; BODY="${2:-}"
|
||||
PLANNER=$(curl -s -H "X-Redmine-API-Key: $REDMINE_API_KEY" \
|
||||
"$REDMINE_URL/projects/$REDMINE_PROJECT/issue_categories.json" \
|
||||
| python3 -c "import sys,json;[print(c['id']) for c in json.load(sys.stdin)['issue_categories'] if c['name']=='01-Planner']")
|
||||
DESC=$(printf '## [AI] Planner\n\n(요구사항)\n%s\n\n---\nWorking dir: %s' "$BODY" "$(pwd)")
|
||||
python3 - "$REDMINE_URL" "$REDMINE_API_KEY" "$REDMINE_PROJECT" "$SUBJECT" "$DESC" "$PLANNER" <<'PY'
|
||||
import sys,json,urllib.request
|
||||
base,key,proj,subject,desc,cat=sys.argv[1:7]
|
||||
p={"issue":{"project_id":proj,"tracker_id":2,"subject":subject,"description":desc,
|
||||
"category_id":int(cat),"status_id":1}}
|
||||
r=urllib.request.Request(base+"/issues.json",data=json.dumps(p).encode(),
|
||||
headers={"X-Redmine-API-Key":key,"Content-Type":"application/json"},method="POST")
|
||||
i=json.load(urllib.request.urlopen(r))["issue"]
|
||||
print(f"enqueued #{i['id']}: {i['subject']} -> 01-Planner/신규")
|
||||
PY
|
||||
32
scripts/provision-dds-token-search.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Provisions the single DDS technical END USER used by business-user searches.
|
||||
# Required environment: DDS_ADMIN_DB_URL, DDS_ADMIN_USERNAME,
|
||||
# DDS_ADMIN_PASSWORD, DDS_TOKEN_PASSWORD.
|
||||
set -Eeuo pipefail
|
||||
|
||||
: "${DDS_ADMIN_DB_URL:?set DDS_ADMIN_DB_URL}"
|
||||
: "${DDS_ADMIN_USERNAME:?set DDS_ADMIN_USERNAME}"
|
||||
: "${DDS_ADMIN_PASSWORD:?set DDS_ADMIN_PASSWORD}"
|
||||
: "${DDS_TOKEN_PASSWORD:?set DDS_TOKEN_PASSWORD}"
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
connect="${DDS_ADMIN_USERNAME}/${DDS_ADMIN_PASSWORD}@${DDS_ADMIN_DB_URL}"
|
||||
|
||||
sqlplus -s "$connect" <<SQL
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
DEFINE DDSUSER_TOKEN_PASSWORD = '${DDS_TOKEN_PASSWORD}'
|
||||
@${root}/sql/adb/34_dds_token_data_grant_common_auth.sql
|
||||
@${root}/sql/adb/42_dds_fga_execution_evidence.sql
|
||||
|
||||
PROMPT === DDS token search verification ===
|
||||
SELECT username FROM dba_users WHERE username = 'dds_demo_token';
|
||||
SELECT grant_name, object_name, grantee
|
||||
FROM dba_data_grants
|
||||
WHERE grant_name = 'DDS_DEMO_TOKEN_VECTOR_GRANT';
|
||||
EXIT
|
||||
SQL
|
||||
|
||||
echo 'Provisioning completed. Configure only these server Secret values:'
|
||||
echo ' DDS_BACKOFFICE_DB_URL=<DB JDBC URL>'
|
||||
echo ' DDS_BACKOFFICE_TOKEN_USERNAME=dds_demo_token'
|
||||
echo ' DDS_BACKOFFICE_TOKEN_PASSWORD=<DDS_TOKEN_PASSWORD value>'
|
||||
53
scripts/run_agent_ords_security_adb_local.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# Agent ORDS security ADB local-only executable example.
|
||||
#
|
||||
# This script does not call RDS, DB Link, Postgres, or MySQL.
|
||||
# It creates local ADB tables/views and verifies VPD + DDS.
|
||||
# ============================================================
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if [[ ! -f "$ROOT/.env" ]]; then
|
||||
echo "[FAIL] .env not found. Copy .env.example to .env and fill ADB_* values." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. "$ROOT/.env"
|
||||
set +a
|
||||
|
||||
run_admin() {
|
||||
local sql_file="$1"
|
||||
echo
|
||||
echo "[ADMIN] @$sql_file"
|
||||
sqlplus -S -L "${ADB_USER}/${ADB_PASSWORD}@${ADB_TNS}" @"$sql_file"
|
||||
}
|
||||
|
||||
run_as() {
|
||||
local user="$1" password="$2" sql_file="$3"
|
||||
echo
|
||||
echo "[$user] @$sql_file"
|
||||
sqlplus -S -L "${user}/${password}@${ADB_TNS}" @"$sql_file"
|
||||
}
|
||||
|
||||
run_admin "$ROOT/sql/adb/16_agent_ords_security_local_cleanup.sql"
|
||||
run_admin "$ROOT/sql/adb/17_agent_ords_security_local_vpd_setup.sql"
|
||||
run_admin "$ROOT/sql/adb/19_agent_ords_security_local_dds_setup.sql"
|
||||
run_admin "$ROOT/sql/adb/21_agent_ords_security_ords_enable_schema.sql"
|
||||
run_as "cb_ords" "CbOrdS#2026Local1" "$ROOT/sql/adb/22_agent_ords_security_ords_handler_setup.sql"
|
||||
run_admin "$ROOT/sql/adb/24_agent_ords_security_inventory.sql"
|
||||
|
||||
run_as "cb_ords" "CbOrdS#2026Local1" "$ROOT/sql/adb/18_agent_ords_security_local_vpd_test.sql"
|
||||
run_as "cb_ords" "CbOrdS#2026Local1" "$ROOT/sql/adb/23_agent_ords_security_ords_handler_test.sql"
|
||||
|
||||
run_as '"cb_dds_hr"' "CbDds#Hr2026Local1" "$ROOT/sql/adb/20_agent_ords_security_local_dds_test.sql"
|
||||
run_as '"cb_dds_fin"' "CbDds#Fin2026Local1" "$ROOT/sql/adb/20_agent_ords_security_local_dds_test.sql"
|
||||
run_as '"cb_dds_all"' "CbDds#All2026Local1" "$ROOT/sql/adb/20_agent_ords_security_local_dds_test.sql"
|
||||
run_as '"cb_dds_none"' "CbDds#None2026Local1" "$ROOT/sql/adb/20_agent_ords_security_local_dds_test.sql"
|
||||
|
||||
echo
|
||||
echo "[OK] Agent ORDS security ADB local-only VPD + DDS executable example complete"
|
||||
94
sql/adb/16_agent_ords_security_local_cleanup.sql
Normal file
@@ -0,0 +1,94 @@
|
||||
-- ============================================================
|
||||
-- 16_agent_ords_security_local_cleanup.sql
|
||||
-- Agent ORDS security local-only cleanup.
|
||||
--
|
||||
-- This removes only CB_* objects used by the local ADB example.
|
||||
-- It does not touch the existing RDS DB Link POC objects.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR CONTINUE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
|
||||
PROMPT === Cleaning VPD policy ===
|
||||
BEGIN
|
||||
DBMS_REDACT.DROP_POLICY(
|
||||
object_schema => USER,
|
||||
object_name => 'CB_V_SEARCH_DOCUMENTS',
|
||||
policy_name => 'CB_CONTENTS_REDACT'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
/
|
||||
|
||||
BEGIN
|
||||
DBMS_RLS.DROP_POLICY(
|
||||
object_schema => USER,
|
||||
object_name => 'CB_V_SEARCH_DOCUMENTS',
|
||||
policy_name => 'CB_AGENT_DOC_POLICY'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === Cleaning DDS data grants ===
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.cb_dg_hr_docs'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.cb_dg_fin_docs'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.cb_dg_all_docs'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
|
||||
PROMPT === Cleaning DDS end users and roles ===
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP END USER "cb_dds_hr"'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP END USER "cb_dds_fin"'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP END USER "cb_dds_all"'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP END USER "cb_dds_none"'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE cb_dds_hr_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE cb_dds_fin_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE cb_dds_all_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE cb_dds_connect_only_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP ROLE cb_dds_connect_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
|
||||
PROMPT === Cleaning DB user and local objects ===
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP USER cb_ords CASCADE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP VIEW cb_dds_v_search_documents'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP VIEW cb_v_search_documents'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP FUNCTION cb_agent_doc_vpd_filter'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP CONTEXT cb_agent_ctx'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP PACKAGE cb_agent_ctx_pkg'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_agent_bearer_key PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_permission_rule PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_permission PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_user_role PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_app_role PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_app_user PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_dds_documents PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_search_documents PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
|
||||
/
|
||||
|
||||
PROMPT === Agent ORDS security local cleanup complete ===
|
||||
EXIT;
|
||||
115
sql/adb/18_agent_ords_security_local_vpd_test.sql
Normal file
@@ -0,0 +1,115 @@
|
||||
-- ============================================================
|
||||
-- 18_agent_ords_security_local_vpd_test.sql
|
||||
-- Run as CB_ORDS.
|
||||
--
|
||||
-- Expected:
|
||||
-- no context -> 0 rows
|
||||
-- cb_hr_key -> HR rows only, 3 rows
|
||||
-- cb_fin_key -> owner_emp_no = E2001 only, 1 row
|
||||
-- cb_all_key -> all rows, 6 rows
|
||||
-- contents column -> NULL unless CAN_READ_CONTENTS = Y
|
||||
-- invalid key -> ORA-20002, then 0 rows
|
||||
-- direct base table -> ORA-00942
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR CONTINUE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET LINESIZE 220
|
||||
SET PAGESIZE 100
|
||||
COLUMN ctx_user_id FORMAT A12
|
||||
COLUMN ctx_emp_no FORMAT A12
|
||||
COLUMN ctx_dept_code FORMAT A12
|
||||
COLUMN ctx_can_read_contents FORMAT A22
|
||||
COLUMN title FORMAT A28
|
||||
COLUMN owner_emp_no FORMAT A12
|
||||
COLUMN dept_code FORMAT A10
|
||||
COLUMN contents FORMAT A35
|
||||
|
||||
PROMPT
|
||||
PROMPT === VPD 1. No Bearer key / context: fail closed ===
|
||||
BEGIN
|
||||
admin.cb_agent_ctx_pkg.clear_user;
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT SYS_CONTEXT('CB_AGENT_CTX','USER_ID') AS ctx_user_id,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','EMP_NO') AS ctx_emp_no,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','DEPT_CODE') AS ctx_dept_code,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','CAN_READ_CONTENTS') AS ctx_can_read_contents
|
||||
FROM dual;
|
||||
|
||||
SELECT COUNT(*) AS rows_visible
|
||||
FROM admin.cb_v_search_documents;
|
||||
|
||||
PROMPT
|
||||
PROMPT === VPD 2. Authorization: Bearer cb_hr_key -> HR department rows ===
|
||||
BEGIN
|
||||
admin.cb_agent_ctx_pkg.set_user_by_bearer('cb_hr_key');
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT SYS_CONTEXT('CB_AGENT_CTX','USER_ID') AS ctx_user_id,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','EMP_NO') AS ctx_emp_no,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','DEPT_CODE') AS ctx_dept_code,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','CAN_READ_CONTENTS') AS ctx_can_read_contents
|
||||
FROM dual;
|
||||
|
||||
SELECT doc_id, title, owner_emp_no, dept_code, contents
|
||||
FROM admin.cb_v_search_documents
|
||||
ORDER BY doc_id;
|
||||
|
||||
SELECT COUNT(*) AS rows_visible
|
||||
FROM admin.cb_v_search_documents;
|
||||
|
||||
PROMPT
|
||||
PROMPT === VPD 3. Authorization: Bearer cb_fin_key -> self row only ===
|
||||
BEGIN
|
||||
admin.cb_agent_ctx_pkg.set_user_by_bearer('cb_fin_key');
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT SYS_CONTEXT('CB_AGENT_CTX','USER_ID') AS ctx_user_id,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','EMP_NO') AS ctx_emp_no,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','DEPT_CODE') AS ctx_dept_code,
|
||||
SYS_CONTEXT('CB_AGENT_CTX','CAN_READ_CONTENTS') AS ctx_can_read_contents
|
||||
FROM dual;
|
||||
|
||||
SELECT doc_id, title, owner_emp_no, dept_code, contents
|
||||
FROM admin.cb_v_search_documents
|
||||
ORDER BY doc_id;
|
||||
|
||||
SELECT COUNT(*) AS rows_visible
|
||||
FROM admin.cb_v_search_documents;
|
||||
|
||||
SELECT doc_id, title, owner_emp_no, dept_code, contents
|
||||
FROM admin.cb_v_search_documents
|
||||
ORDER BY doc_id
|
||||
FETCH FIRST 3 ROWS ONLY;
|
||||
|
||||
PROMPT
|
||||
PROMPT === VPD 4. Authorization: Bearer cb_all_key -> all rows ===
|
||||
BEGIN
|
||||
admin.cb_agent_ctx_pkg.set_user_by_bearer('cb_all_key');
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT COUNT(*) AS rows_visible
|
||||
FROM admin.cb_v_search_documents;
|
||||
|
||||
PROMPT
|
||||
PROMPT === VPD 5. Invalid Bearer key -> ORA-20002 and context cleared ===
|
||||
BEGIN
|
||||
admin.cb_agent_ctx_pkg.set_user_by_bearer('wrong_key');
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT COUNT(*) AS rows_visible_after_invalid_key
|
||||
FROM admin.cb_v_search_documents;
|
||||
|
||||
PROMPT
|
||||
PROMPT === VPD 6. Bypass attempts: direct base/permission tables are not granted ===
|
||||
SELECT COUNT(*) FROM admin.cb_search_documents;
|
||||
SELECT COUNT(*) FROM admin.cb_app_user;
|
||||
SELECT COUNT(*) FROM admin.cb_agent_bearer_key;
|
||||
|
||||
EXIT;
|
||||
89
sql/adb/19_agent_ords_security_local_dds_setup.sql
Normal file
@@ -0,0 +1,89 @@
|
||||
-- ============================================================
|
||||
-- 19_agent_ords_security_local_dds_setup.sql
|
||||
-- Local-only executable DDS example for Agent ORDS security.
|
||||
--
|
||||
-- Scenario:
|
||||
-- * DDS uses END USER -> DATA ROLE -> DATA GRANT.
|
||||
-- * No permission table lookup is used.
|
||||
-- * Each DATA GRANT declares the protected object, row filter,
|
||||
-- and column restriction.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET DEFINE OFF
|
||||
|
||||
PROMPT === 1. Creating DDS-only local table and view ===
|
||||
CREATE TABLE cb_dds_documents (
|
||||
doc_id NUMBER PRIMARY KEY,
|
||||
title VARCHAR2(100) NOT NULL,
|
||||
owner_emp_no VARCHAR2(20) NOT NULL,
|
||||
dept_code VARCHAR2(20) NOT NULL,
|
||||
contents VARCHAR2(4000),
|
||||
created_at DATE DEFAULT SYSDATE NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO cb_dds_documents VALUES (1, 'HR payroll guide', 'E1001', 'HR', 'Payroll policy and HR guide', SYSDATE);
|
||||
INSERT INTO cb_dds_documents VALUES (2, 'HR recruiting plan', 'E1002', 'HR', 'Recruiting plan for HR team', SYSDATE);
|
||||
INSERT INTO cb_dds_documents VALUES (3, 'Finance close checklist','E2001', 'FIN', 'Monthly close checklist', SYSDATE);
|
||||
INSERT INTO cb_dds_documents VALUES (4, 'Finance audit memo', 'E2002', 'FIN', 'Audit memo for finance team', SYSDATE);
|
||||
INSERT INTO cb_dds_documents VALUES (5, 'Sales forecast', 'E3001', 'SALES', 'Quarterly sales forecast', SYSDATE);
|
||||
INSERT INTO cb_dds_documents VALUES (6, 'HR benefits notice', 'E1003', 'HR', 'Benefits notice for employees', SYSDATE);
|
||||
|
||||
CREATE OR REPLACE VIEW cb_dds_v_search_documents AS
|
||||
SELECT doc_id,
|
||||
title,
|
||||
owner_emp_no,
|
||||
dept_code,
|
||||
contents,
|
||||
created_at
|
||||
FROM cb_dds_documents;
|
||||
|
||||
COMMIT;
|
||||
|
||||
PROMPT === 2. Creating DDS end users ===
|
||||
CREATE END USER "cb_dds_hr" IDENTIFIED BY "CbDds#Hr2026Local1";
|
||||
CREATE END USER "cb_dds_fin" IDENTIFIED BY "CbDds#Fin2026Local1";
|
||||
CREATE END USER "cb_dds_all" IDENTIFIED BY "CbDds#All2026Local1";
|
||||
CREATE END USER "cb_dds_none" IDENTIFIED BY "CbDds#None2026Local1";
|
||||
|
||||
PROMPT === 3. Creating DATA ROLEs and connection carrier role ===
|
||||
CREATE ROLE cb_dds_connect_role;
|
||||
GRANT CREATE SESSION TO cb_dds_connect_role;
|
||||
|
||||
CREATE DATA ROLE cb_dds_hr_role;
|
||||
CREATE DATA ROLE cb_dds_fin_role;
|
||||
CREATE DATA ROLE cb_dds_all_role;
|
||||
CREATE DATA ROLE cb_dds_connect_only_role;
|
||||
|
||||
GRANT cb_dds_connect_role TO cb_dds_hr_role;
|
||||
GRANT cb_dds_connect_role TO cb_dds_fin_role;
|
||||
GRANT cb_dds_connect_role TO cb_dds_all_role;
|
||||
GRANT cb_dds_connect_role TO cb_dds_connect_only_role;
|
||||
|
||||
PROMPT === 4. Mapping END USERs to DATA ROLEs ===
|
||||
GRANT DATA ROLE cb_dds_hr_role TO "cb_dds_hr";
|
||||
GRANT DATA ROLE cb_dds_fin_role TO "cb_dds_fin";
|
||||
GRANT DATA ROLE cb_dds_all_role TO "cb_dds_all";
|
||||
GRANT DATA ROLE cb_dds_connect_only_role TO "cb_dds_none";
|
||||
|
||||
PROMPT === 5. Creating DATA GRANTs ===
|
||||
CREATE DATA GRANT admin.cb_dg_hr_docs
|
||||
AS SELECT (ALL COLUMNS EXCEPT contents)
|
||||
ON admin.cb_dds_v_search_documents
|
||||
WHERE dept_code = 'HR'
|
||||
TO cb_dds_hr_role;
|
||||
|
||||
CREATE DATA GRANT admin.cb_dg_fin_docs
|
||||
AS SELECT (ALL COLUMNS EXCEPT contents)
|
||||
ON admin.cb_dds_v_search_documents
|
||||
WHERE dept_code = 'FIN'
|
||||
TO cb_dds_fin_role;
|
||||
|
||||
CREATE DATA GRANT admin.cb_dg_all_docs
|
||||
AS SELECT
|
||||
ON admin.cb_dds_v_search_documents
|
||||
TO cb_dds_all_role;
|
||||
|
||||
PROMPT === DDS local setup complete ===
|
||||
EXIT;
|
||||
46
sql/adb/20_agent_ords_security_local_dds_test.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- ============================================================
|
||||
-- 20_agent_ords_security_local_dds_test.sql
|
||||
-- Run as cb_dds_hr / cb_dds_fin / cb_dds_all / cb_dds_none.
|
||||
--
|
||||
-- Expected:
|
||||
-- cb_dds_hr -> 3 rows from admin.cb_dds_v_search_documents
|
||||
-- cb_dds_fin -> 2 rows from admin.cb_dds_v_search_documents
|
||||
-- cb_dds_all -> 6 rows from admin.cb_dds_v_search_documents
|
||||
-- contents -> NULL for hr/fin grants, visible for all grant
|
||||
-- cb_dds_none -> ORA-00942 on admin.cb_dds_v_search_documents
|
||||
-- base table -> ORA-00942 for every DDS end user
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR CONTINUE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET LINESIZE 220
|
||||
SET PAGESIZE 100
|
||||
COLUMN end_user_name FORMAT A16
|
||||
COLUMN title FORMAT A28
|
||||
COLUMN owner_emp_no FORMAT A12
|
||||
COLUMN dept_code FORMAT A10
|
||||
COLUMN contents FORMAT A35
|
||||
|
||||
PROMPT
|
||||
PROMPT === DDS 1. End user context ===
|
||||
SELECT ORA_END_USER_CONTEXT.username AS end_user_name
|
||||
FROM dual;
|
||||
|
||||
PROMPT
|
||||
PROMPT === DDS 2. Protected view row count ===
|
||||
SELECT COUNT(*) AS rows_visible
|
||||
FROM admin.cb_dds_v_search_documents;
|
||||
|
||||
PROMPT
|
||||
PROMPT === DDS 3. Protected view sample ===
|
||||
SELECT doc_id, title, owner_emp_no, dept_code, contents
|
||||
FROM admin.cb_dds_v_search_documents
|
||||
ORDER BY doc_id;
|
||||
|
||||
PROMPT
|
||||
PROMPT === DDS 4. Bypass attempts: no DATA GRANT on base/VPD objects ===
|
||||
SELECT COUNT(*) FROM admin.cb_dds_documents;
|
||||
SELECT COUNT(*) FROM admin.cb_search_documents;
|
||||
SELECT COUNT(*) FROM admin.cb_v_search_documents;
|
||||
|
||||
EXIT;
|
||||
25
sql/adb/21_agent_ords_security_ords_enable_schema.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- ============================================================
|
||||
-- 21_agent_ords_security_ords_enable_schema.sql
|
||||
-- Enable the CB_ORDS schema for ORDS module/handler definitions.
|
||||
--
|
||||
-- Run as ADMIN after CB_ORDS has been created.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
|
||||
PROMPT === Enabling CB_ORDS as an ORDS schema ===
|
||||
BEGIN
|
||||
ORDS.ENABLE_SCHEMA(
|
||||
p_enabled => TRUE,
|
||||
p_schema => 'CB_ORDS',
|
||||
p_url_mapping_type => 'BASE_PATH',
|
||||
p_url_mapping_pattern => 'cb-ords',
|
||||
p_auto_rest_auth => FALSE
|
||||
);
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === CB_ORDS ORDS schema enabled ===
|
||||
EXIT;
|
||||
44
sql/adb/23_agent_ords_security_ords_handler_test.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- ============================================================
|
||||
-- 23_agent_ords_security_ords_handler_test.sql
|
||||
-- Run as CB_ORDS.
|
||||
--
|
||||
-- This directly executes the same package that the ORDS handlers
|
||||
-- call. It proves the database-side behavior without requiring an
|
||||
-- external ORDS URL.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR CONTINUE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET LONG 100000
|
||||
SET LONGCHUNKSIZE 100000
|
||||
SET LINESIZE 220
|
||||
SET PAGESIZE 100
|
||||
SET SERVEROUTPUT ON
|
||||
COLUMN response FORMAT A180 WORD_WRAPPED
|
||||
|
||||
PROMPT
|
||||
PROMPT === ORDS handler probe 1. VPD path with mandatory Bearer header ===
|
||||
SELECT cb_ords_handler_pkg.vpd_search_json('Bearer cb_hr_key') AS response
|
||||
FROM dual;
|
||||
|
||||
PROMPT
|
||||
PROMPT === ORDS handler probe 2. VPD path without Bearer header ===
|
||||
BEGIN
|
||||
DBMS_OUTPUT.PUT_LINE(cb_ords_handler_pkg.vpd_search_json(NULL));
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
DBMS_OUTPUT.PUT_LINE(SQLERRM);
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT
|
||||
PROMPT === ORDS handler probe 3. DDS path with Bearer header only ===
|
||||
SELECT cb_ords_handler_pkg.dds_bearer_probe_json('Bearer cb_hr_key') AS response
|
||||
FROM dual;
|
||||
|
||||
PROMPT
|
||||
PROMPT === ORDS handler probe 4. DDS path with all-access Bearer header only ===
|
||||
SELECT cb_ords_handler_pkg.dds_bearer_probe_json('Bearer cb_all_key') AS response
|
||||
FROM dual;
|
||||
|
||||
EXIT;
|
||||
99
sql/adb/24_agent_ords_security_inventory.sql
Normal file
@@ -0,0 +1,99 @@
|
||||
-- ============================================================
|
||||
-- 24_agent_ords_security_inventory.sql
|
||||
-- Central security inventory for Agent ORDS security.
|
||||
--
|
||||
-- Run as ADMIN after VPD, DDS, and ORDS handler setup.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR CONTINUE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET LINESIZE 260
|
||||
SET PAGESIZE 200
|
||||
|
||||
COLUMN object_owner FORMAT A12
|
||||
COLUMN object_name FORMAT A32
|
||||
COLUMN policy_name FORMAT A26
|
||||
COLUMN pf_owner FORMAT A12
|
||||
COLUMN function FORMAT A30
|
||||
COLUMN sel FORMAT A5
|
||||
COLUMN ins FORMAT A5
|
||||
COLUMN upd FORMAT A5
|
||||
COLUMN del FORMAT A5
|
||||
COLUMN enable FORMAT A8
|
||||
COLUMN expression FORMAT A70 WORD_WRAPPED
|
||||
COLUMN grant_name FORMAT A24
|
||||
COLUMN grantee FORMAT A28
|
||||
COLUMN privilege FORMAT A10
|
||||
COLUMN allowed_columns FORMAT A76 WORD_WRAPPED
|
||||
COLUMN excluded_columns FORMAT A22
|
||||
COLUMN predicate FORMAT A28
|
||||
COLUMN end_user_name FORMAT A26
|
||||
COLUMN data_role FORMAT A28
|
||||
|
||||
PROMPT
|
||||
PROMPT === Inventory 1. VPD policies attached to protected objects ===
|
||||
SELECT object_owner,
|
||||
object_name,
|
||||
policy_name,
|
||||
pf_owner,
|
||||
function,
|
||||
sel,
|
||||
ins,
|
||||
upd,
|
||||
del,
|
||||
enable
|
||||
FROM dba_policies
|
||||
WHERE object_owner = 'ADMIN'
|
||||
AND object_name LIKE 'CB%'
|
||||
ORDER BY object_name, policy_name;
|
||||
|
||||
PROMPT
|
||||
PROMPT === Inventory 2. Redaction policies attached to protected objects ===
|
||||
SELECT object_owner,
|
||||
object_name,
|
||||
policy_name,
|
||||
enable,
|
||||
expression
|
||||
FROM redaction_policies
|
||||
WHERE object_owner = 'ADMIN'
|
||||
AND object_name LIKE 'CB%'
|
||||
ORDER BY object_name, policy_name;
|
||||
|
||||
PROMPT
|
||||
PROMPT === Inventory 3. DDS grant summary, one row per DATA GRANT ===
|
||||
SELECT grant_name,
|
||||
object_name,
|
||||
grantee,
|
||||
privilege,
|
||||
COALESCE(
|
||||
LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_name),
|
||||
'ALL COLUMNS'
|
||||
) AS allowed_columns,
|
||||
COALESCE(MAX(granted_with_all_columns_except), '-') AS excluded_columns,
|
||||
predicate
|
||||
FROM dba_data_grants
|
||||
WHERE owner = 'ADMIN'
|
||||
AND object_name LIKE 'CB%'
|
||||
GROUP BY grant_name, object_name, grantee, privilege, predicate
|
||||
ORDER BY object_name, grant_name;
|
||||
|
||||
PROMPT
|
||||
PROMPT === Inventory 4. DDS end user -> data role -> data grant matrix ===
|
||||
SELECT rg.grantee AS end_user_name,
|
||||
rg.data_role,
|
||||
dg.grant_name,
|
||||
dg.object_name,
|
||||
dg.predicate,
|
||||
COALESCE(MAX(dg.granted_with_all_columns_except), '-') AS excluded_columns
|
||||
FROM dba_data_role_grants rg
|
||||
LEFT JOIN dba_data_grants dg
|
||||
ON dg.grantee = rg.data_role
|
||||
WHERE rg.grantee LIKE 'cb\_dds\_%' ESCAPE '\'
|
||||
GROUP BY rg.grantee,
|
||||
rg.data_role,
|
||||
dg.grant_name,
|
||||
dg.object_name,
|
||||
dg.predicate
|
||||
ORDER BY rg.grantee, dg.object_name, dg.grant_name;
|
||||
|
||||
EXIT;
|
||||
24
sql/adb/41_dds_row_column_fixture.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- DDS row/column verification fixtures. Run as ADMIN after 31 and 34.
|
||||
CREATE TABLE admin.cb_dds_security_fixture (
|
||||
fixture_id NUMBER PRIMARY KEY,
|
||||
dept_code VARCHAR2(30) NOT NULL,
|
||||
owner_emp_no VARCHAR2(30) NOT NULL,
|
||||
tech_tag VARCHAR2(100) NOT NULL,
|
||||
title VARCHAR2(200) NOT NULL,
|
||||
contents VARCHAR2(2000) NOT NULL,
|
||||
public_summary VARCHAR2(500) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO admin.cb_dds_security_fixture VALUES (1,'SALES','E2001','SALES','영업 파이프라인','영업 원문','영업 요약');
|
||||
INSERT INTO admin.cb_dds_security_fixture VALUES (2,'HR','E10234','HR','인사 운영','인사 원문','인사 요약');
|
||||
INSERT INTO admin.cb_dds_security_fixture VALUES (3,'FIN','E99999','FINANCE','재무 마감','재무 원문','재무 요약');
|
||||
COMMIT;
|
||||
|
||||
CREATE OR REPLACE DATA GRANT admin.dds_demo_token_fixture_grant
|
||||
AS SELECT (fixture_id, dept_code, owner_emp_no, tech_tag, title, public_summary)
|
||||
ON admin.cb_dds_security_fixture
|
||||
WHERE admin.cb_dds_vector_tag_allowed(tech_tag) = 1
|
||||
TO cb_dds_token_role;
|
||||
|
||||
-- Expected: token mapped to SALES sees row 1; HR/FIN rows are denied.
|
||||
-- CONTENTS is deliberately excluded: this is the column-level deny case.
|
||||
32
sql/adb/42_dds_fga_execution_evidence.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- DDS execution evidence: durable FGA audit for the protected vector view.
|
||||
-- Run as ADMIN/AUDIT_ADMIN. In unified auditing the records are queried from
|
||||
-- UNIFIED_AUDIT_TRAIL with AUDIT_TYPE = 'FineGrainedAudit'.
|
||||
BEGIN
|
||||
DBMS_FGA.DROP_POLICY(
|
||||
object_schema => 'ADMIN', object_name => 'CB_DDS_VECTOR_SEARCH_DOCUMENTS',
|
||||
policy_name => 'DDS_VECTOR_EXECUTION_EVIDENCE');
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
IF SQLCODE != -28102 THEN RAISE; END IF;
|
||||
END;
|
||||
/
|
||||
BEGIN
|
||||
DBMS_FGA.ADD_POLICY(
|
||||
object_schema => 'ADMIN',
|
||||
object_name => 'CB_DDS_VECTOR_SEARCH_DOCUMENTS',
|
||||
policy_name => 'DDS_VECTOR_EXECUTION_EVIDENCE',
|
||||
audit_condition => NULL,
|
||||
audit_column => NULL,
|
||||
statement_types => 'SELECT',
|
||||
audit_trail => DBMS_FGA.DB_EXTENDED,
|
||||
enable => TRUE);
|
||||
END;
|
||||
/
|
||||
-- Evidence lookup (grant AUDIT_VIEWER or expose a definer-rights reader view
|
||||
-- to the backoffice; never grant this to DDS END USER accounts):
|
||||
-- SELECT event_timestamp, client_identifier, dbusername, object_name,
|
||||
-- return_code, sql_text, application_contexts
|
||||
-- FROM unified_audit_trail
|
||||
-- WHERE audit_type = 'FineGrainedAudit'
|
||||
-- AND object_schema = 'ADMIN'
|
||||
-- AND object_name = 'CB_DDS_VECTOR_SEARCH_DOCUMENTS'
|
||||
-- ORDER BY event_timestamp DESC;
|
||||
@@ -432,88 +432,27 @@ BEGIN
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === 5. Applying column NULL policies from permission display exceptions ===
|
||||
-- ADB rejects application PL/SQL functions inside DBMS_REDACT expressions.
|
||||
-- SEC_RELEVANT_COL_OPT=ALL_ROWS is the VPD equivalent: it retains the row
|
||||
-- but returns NULL for the protected column while the column predicate is false.
|
||||
CREATE OR REPLACE FUNCTION cb_kb_cust_nm_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2)
|
||||
RETURN VARCHAR2 AUTHID DEFINER AS
|
||||
BEGIN
|
||||
RETURN CASE WHEN cb_agent_can_read_column('KB_CUSTOMERS', 'CUST_NM') = 'Y' THEN '1 = 1' ELSE '1 = 0' END;
|
||||
END;
|
||||
/
|
||||
CREATE OR REPLACE FUNCTION cb_kb_rrn_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2)
|
||||
RETURN VARCHAR2 AUTHID DEFINER AS
|
||||
BEGIN
|
||||
RETURN CASE WHEN cb_agent_can_read_column('KB_CUSTOMERS', 'RRN_MASKED') = 'Y' THEN '1 = 1' ELSE '1 = 0' END;
|
||||
END;
|
||||
/
|
||||
CREATE OR REPLACE FUNCTION cb_kb_claim_amt_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2)
|
||||
RETURN VARCHAR2 AUTHID DEFINER AS
|
||||
BEGIN
|
||||
RETURN CASE WHEN cb_agent_can_read_column('KB_CLAIMS', 'CLAIM_AMT') = 'Y' THEN '1 = 1' ELSE '1 = 0' END;
|
||||
END;
|
||||
/
|
||||
CREATE OR REPLACE FUNCTION cb_kb_paid_amt_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2)
|
||||
RETURN VARCHAR2 AUTHID DEFINER AS
|
||||
BEGIN
|
||||
RETURN CASE WHEN cb_agent_can_read_column('KB_CLAIMS', 'PAID_AMT') = 'Y' THEN '1 = 1' ELSE '1 = 0' END;
|
||||
END;
|
||||
/
|
||||
CREATE OR REPLACE FUNCTION cb_kb_ext_insurer_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2)
|
||||
RETURN VARCHAR2 AUTHID DEFINER AS
|
||||
BEGIN
|
||||
RETURN CASE WHEN cb_agent_can_read_column('KB_EXTERNAL_HOLDINGS', 'EXT_INSURER') = 'Y' THEN '1 = 1' ELSE '1 = 0' END;
|
||||
END;
|
||||
/
|
||||
CREATE OR REPLACE FUNCTION cb_kb_ext_product_grp_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2)
|
||||
RETURN VARCHAR2 AUTHID DEFINER AS
|
||||
BEGIN
|
||||
RETURN CASE WHEN cb_agent_can_read_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_GRP') = 'Y' THEN '1 = 1' ELSE '1 = 0' END;
|
||||
END;
|
||||
/
|
||||
CREATE OR REPLACE FUNCTION cb_kb_ext_product_type_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2)
|
||||
RETURN VARCHAR2 AUTHID DEFINER AS
|
||||
BEGIN
|
||||
RETURN CASE WHEN cb_agent_can_read_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_TYPE') = 'Y' THEN '1 = 1' ELSE '1 = 0' END;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === 5. Removing deprecated VPD column NULL policies ===
|
||||
-- Architecture rule: VPD applies row predicates only. ASO/Data Redaction
|
||||
-- owns all column masking and its user-specific original-value exceptions.
|
||||
DECLARE
|
||||
PROCEDURE replace_column_policy(
|
||||
p_object_name IN VARCHAR2,
|
||||
p_column_name IN VARCHAR2,
|
||||
p_policy_name IN VARCHAR2,
|
||||
p_function_name IN VARCHAR2
|
||||
) IS
|
||||
PROCEDURE drop_column_policy(p_object_name IN VARCHAR2, p_policy_name IN VARCHAR2) IS
|
||||
BEGIN
|
||||
BEGIN
|
||||
DBMS_RLS.DROP_POLICY(
|
||||
object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
DBMS_RLS.ADD_POLICY(
|
||||
object_schema => 'POC_2',
|
||||
object_name => p_object_name,
|
||||
policy_name => p_policy_name,
|
||||
function_schema => USER,
|
||||
policy_function => p_function_name,
|
||||
statement_types => 'SELECT',
|
||||
policy_type => DBMS_RLS.DYNAMIC,
|
||||
sec_relevant_cols => p_column_name,
|
||||
sec_relevant_cols_opt => DBMS_RLS.ALL_ROWS,
|
||||
enable => TRUE
|
||||
DBMS_RLS.DROP_POLICY(
|
||||
object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -28102 THEN RAISE; END IF;
|
||||
END;
|
||||
BEGIN
|
||||
replace_column_policy('KB_CUSTOMERS', 'CUST_NM', 'KB_CUST_NM_CLS_POLICY', 'CB_KB_CUST_NM_CLS');
|
||||
replace_column_policy('KB_CUSTOMERS', 'RRN_MASKED', 'KB_RRN_CLS_POLICY', 'CB_KB_RRN_CLS');
|
||||
replace_column_policy('KB_CLAIMS', 'CLAIM_AMT', 'KB_CLAIM_AMT_CLS_POLICY', 'CB_KB_CLAIM_AMT_CLS');
|
||||
replace_column_policy('KB_CLAIMS', 'PAID_AMT', 'KB_PAID_AMT_CLS_POLICY', 'CB_KB_PAID_AMT_CLS');
|
||||
replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_INSURER', 'KB_EXT_INSURER_CLS_POLICY', 'CB_KB_EXT_INSURER_CLS');
|
||||
replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_GRP', 'KB_EXT_PRODUCT_GRP_CLS_POLICY', 'CB_KB_EXT_PRODUCT_GRP_CLS');
|
||||
replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_TYPE', 'KB_EXT_PRODUCT_TYPE_CLS_POLICY', 'CB_KB_EXT_PRODUCT_TYPE_CLS');
|
||||
drop_column_policy('KB_CUSTOMERS', 'KB_CUST_NM_CLS_POLICY');
|
||||
drop_column_policy('KB_CUSTOMERS', 'KB_RRN_CLS_POLICY');
|
||||
drop_column_policy('KB_CLAIMS', 'KB_CLAIM_AMT_CLS_POLICY');
|
||||
drop_column_policy('KB_CLAIMS', 'KB_PAID_AMT_CLS_POLICY');
|
||||
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_INSURER_CLS_POLICY');
|
||||
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_PRODUCT_GRP_CLS_POLICY');
|
||||
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_PRODUCT_TYPE_CLS_POLICY');
|
||||
END;
|
||||
/
|
||||
|
||||
|
||||
@@ -65,13 +65,6 @@ BEGIN
|
||||
require_runtime_object('CB_AGENT_CTX_PKG', 'PACKAGE BODY');
|
||||
require_runtime_object('CB_AGENT_DOC_VPD_FILTER', 'FUNCTION');
|
||||
require_runtime_object('CB_AGENT_CAN_READ_COLUMN', 'FUNCTION');
|
||||
require_runtime_object('CB_KB_CUST_NM_CLS', 'FUNCTION');
|
||||
require_runtime_object('CB_KB_RRN_CLS', 'FUNCTION');
|
||||
require_runtime_object('CB_KB_CLAIM_AMT_CLS', 'FUNCTION');
|
||||
require_runtime_object('CB_KB_PAID_AMT_CLS', 'FUNCTION');
|
||||
require_runtime_object('CB_KB_EXT_INSURER_CLS', 'FUNCTION');
|
||||
require_runtime_object('CB_KB_EXT_PRODUCT_GRP_CLS', 'FUNCTION');
|
||||
require_runtime_object('CB_KB_EXT_PRODUCT_TYPE_CLS', 'FUNCTION');
|
||||
END;
|
||||
/
|
||||
|
||||
@@ -128,50 +121,28 @@ BEGIN
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === 4. Reapplying column NULL policies ===
|
||||
PROMPT === 4. Removing deprecated VPD column NULL policies ===
|
||||
-- Architecture rule: VPD filters rows only. Column masking is handled solely
|
||||
-- by ASO / DBMS_REDACT through 63_kb_aso_masking_rule_runtime.sql.
|
||||
DECLARE
|
||||
PROCEDURE replace_column_policy(
|
||||
p_object_name IN VARCHAR2,
|
||||
p_column_name IN VARCHAR2,
|
||||
p_policy_name IN VARCHAR2,
|
||||
p_function_name IN VARCHAR2
|
||||
) IS
|
||||
v_count NUMBER;
|
||||
PROCEDURE drop_column_policy(p_object_name IN VARCHAR2, p_policy_name IN VARCHAR2) IS
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_count
|
||||
FROM all_policies
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name = p_object_name
|
||||
AND policy_name = p_policy_name;
|
||||
IF v_count > 0 THEN
|
||||
DBMS_RLS.DROP_POLICY(
|
||||
object_schema => 'POC_2',
|
||||
object_name => p_object_name,
|
||||
policy_name => p_policy_name
|
||||
);
|
||||
END IF;
|
||||
DBMS_RLS.ADD_POLICY(
|
||||
object_schema => 'POC_2',
|
||||
object_name => p_object_name,
|
||||
policy_name => p_policy_name,
|
||||
function_schema => 'ADMIN',
|
||||
policy_function => p_function_name,
|
||||
statement_types => 'SELECT',
|
||||
policy_type => DBMS_RLS.DYNAMIC,
|
||||
sec_relevant_cols => p_column_name,
|
||||
sec_relevant_cols_opt => DBMS_RLS.ALL_ROWS,
|
||||
enable => TRUE
|
||||
DBMS_RLS.DROP_POLICY(
|
||||
object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- A missing policy is already the desired state. Do not hide other errors.
|
||||
IF SQLCODE != -28102 THEN RAISE; END IF;
|
||||
END;
|
||||
BEGIN
|
||||
replace_column_policy('KB_CUSTOMERS', 'CUST_NM', 'KB_CUST_NM_CLS_POLICY', 'CB_KB_CUST_NM_CLS');
|
||||
replace_column_policy('KB_CUSTOMERS', 'RRN_MASKED', 'KB_RRN_CLS_POLICY', 'CB_KB_RRN_CLS');
|
||||
replace_column_policy('KB_CLAIMS', 'CLAIM_AMT', 'KB_CLAIM_AMT_CLS_POLICY', 'CB_KB_CLAIM_AMT_CLS');
|
||||
replace_column_policy('KB_CLAIMS', 'PAID_AMT', 'KB_PAID_AMT_CLS_POLICY', 'CB_KB_PAID_AMT_CLS');
|
||||
replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_INSURER', 'KB_EXT_INSURER_CLS_POLICY', 'CB_KB_EXT_INSURER_CLS');
|
||||
replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_GRP', 'KB_EXT_PRODUCT_GRP_CLS_POLICY', 'CB_KB_EXT_PRODUCT_GRP_CLS');
|
||||
replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_TYPE', 'KB_EXT_PRODUCT_TYPE_CLS_POLICY', 'CB_KB_EXT_PRODUCT_TYPE_CLS');
|
||||
drop_column_policy('KB_CUSTOMERS', 'KB_CUST_NM_CLS_POLICY');
|
||||
drop_column_policy('KB_CUSTOMERS', 'KB_RRN_CLS_POLICY');
|
||||
drop_column_policy('KB_CLAIMS', 'KB_CLAIM_AMT_CLS_POLICY');
|
||||
drop_column_policy('KB_CLAIMS', 'KB_PAID_AMT_CLS_POLICY');
|
||||
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_INSURER_CLS_POLICY');
|
||||
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_PRODUCT_GRP_CLS_POLICY');
|
||||
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_PRODUCT_TYPE_CLS_POLICY');
|
||||
END;
|
||||
/
|
||||
|
||||
|
||||
100
sql/adb/62_kb_aso_masking_backoffice_metadata.sql
Normal file
@@ -0,0 +1,100 @@
|
||||
-- ============================================================
|
||||
-- 62_kb_aso_masking_backoffice_metadata.sql
|
||||
--
|
||||
-- Creates the metadata used by the masking-rule backoffice pages.
|
||||
-- Run as ADMIN with SQLcl. This script does not create a DBMS_REDACT
|
||||
-- policy; run 63_kb_aso_masking_rule_runtime.sql after rules are
|
||||
-- connected to protected columns.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET DEFINE OFF
|
||||
SET FEEDBACK ON
|
||||
|
||||
PROMPT === Creating masking-rule metadata tables ===
|
||||
DECLARE
|
||||
PROCEDURE ensure_table(p_name IN VARCHAR2, p_ddl IN CLOB) IS
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE p_ddl;
|
||||
DBMS_OUTPUT.PUT_LINE('created ' || p_name);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE = -955 THEN
|
||||
DBMS_OUTPUT.PUT_LINE('exists ' || p_name);
|
||||
ELSE
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
BEGIN
|
||||
ensure_table('CB_MASKING_RULE', q'[
|
||||
CREATE TABLE cb_masking_rule (
|
||||
rule_id NUMBER PRIMARY KEY,
|
||||
rule_code VARCHAR2(64) NOT NULL UNIQUE,
|
||||
rule_name VARCHAR2(100) NOT NULL,
|
||||
template_code VARCHAR2(30) NOT NULL,
|
||||
description VARCHAR2(400),
|
||||
enabled_yn CHAR(1) DEFAULT 'Y' CHECK (enabled_yn IN ('Y','N')) NOT NULL
|
||||
)]');
|
||||
|
||||
ensure_table('CB_COLUMN_MASKING_RULE', q'[
|
||||
CREATE TABLE cb_column_masking_rule (
|
||||
column_id NUMBER PRIMARY KEY,
|
||||
rule_id NUMBER NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||
)]');
|
||||
|
||||
ensure_table('CB_USER_MASKING_RULE', q'[
|
||||
CREATE TABLE cb_user_masking_rule (
|
||||
user_id NUMBER NOT NULL,
|
||||
column_id NUMBER NOT NULL,
|
||||
decision VARCHAR2(10) NOT NULL CHECK (decision IN ('MASK','UNMASK')),
|
||||
active_yn CHAR(1) DEFAULT 'Y' CHECK (active_yn IN ('Y','N')) NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT cb_user_masking_rule_pk PRIMARY KEY (user_id, column_id)
|
||||
)]');
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === Seeding curated masking templates ===
|
||||
DECLARE
|
||||
v_next_rule_id NUMBER;
|
||||
|
||||
PROCEDURE ensure_rule(
|
||||
p_code IN VARCHAR2,
|
||||
p_name IN VARCHAR2,
|
||||
p_template IN VARCHAR2,
|
||||
p_description IN VARCHAR2
|
||||
) IS
|
||||
BEGIN
|
||||
MERGE INTO cb_masking_rule dst
|
||||
USING (
|
||||
SELECT p_code rule_code,
|
||||
p_name rule_name,
|
||||
p_template template_code,
|
||||
p_description description
|
||||
FROM dual
|
||||
) src
|
||||
ON (dst.rule_code = src.rule_code)
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (rule_id, rule_code, rule_name, template_code, description, enabled_yn)
|
||||
VALUES (v_next_rule_id, src.rule_code, src.rule_name, src.template_code, src.description, 'Y');
|
||||
v_next_rule_id := v_next_rule_id + 1;
|
||||
END;
|
||||
BEGIN
|
||||
SELECT NVL(MAX(rule_id), 0) + 1 INTO v_next_rule_id FROM cb_masking_rule;
|
||||
ensure_rule('MASK_NULLIFY', '값 숨김 (NULL)', 'NULLIFY',
|
||||
'값을 NULL로 반환하는 기본 마스킹 방식');
|
||||
ensure_rule('MASK_FULL', '전체 마스킹', 'FULL',
|
||||
'문자형은 공백, 숫자형은 0으로 반환하는 전체 마스킹 방식');
|
||||
ensure_rule('MASK_TEXT_PARTIAL', '문자열 일부 마스킹', 'TEXT_PARTIAL',
|
||||
'첫 글자만 보이고 나머지는 가리는 문자열 마스킹 방식');
|
||||
ensure_rule('MASK_RRN_PARTIAL', '주민등록번호 부분 마스킹', 'RRN_PARTIAL',
|
||||
'앞 6자리만 보이고 나머지는 가리는 식별번호 마스킹 방식');
|
||||
END;
|
||||
/
|
||||
COMMIT;
|
||||
|
||||
PROMPT === Metadata ready ===
|
||||
SELECT rule_id, rule_code, rule_name, template_code, enabled_yn
|
||||
FROM cb_masking_rule
|
||||
ORDER BY rule_id;
|
||||
EXIT
|
||||
679
sql/adb/63_kb_aso_masking_rule_runtime.sql
Normal file
@@ -0,0 +1,679 @@
|
||||
-- ============================================================
|
||||
-- 63_kb_aso_masking_rule_runtime.sql
|
||||
--
|
||||
-- Synchronizes CB_MASKING_RULE / CB_COLUMN_MASKING_RULE and
|
||||
-- CB_USER_MASKING_RULE into Oracle Data Redaction policies.
|
||||
--
|
||||
-- Important behaviour
|
||||
-- * One selected masking rule is applied to each protected column.
|
||||
-- * A direct user rule controls whether that user is masked or receives
|
||||
-- an original-value exception (UNMASK).
|
||||
-- * VPD still determines row visibility. Existing VPD NULL policies
|
||||
-- remain in force for users without column permission.
|
||||
--
|
||||
-- Run as ADMIN with SQLcl after 62 to initialize the runtime package, or as
|
||||
-- a manual recovery/reconciliation action. The backoffice synchronizes
|
||||
-- DBMS_REDACT on normal configuration changes; this script follows the same
|
||||
-- source-of-truth rule for existing installations.
|
||||
-- Requires EXECUTE on DBMS_REDACT and ADMINISTER REDACTION POLICY for POC_2
|
||||
-- objects.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET DEFINE OFF
|
||||
SET FEEDBACK ON
|
||||
|
||||
PROMPT === Validating masking-rule metadata ===
|
||||
DECLARE
|
||||
PROCEDURE require_table(p_table_name IN VARCHAR2) IS
|
||||
v_count NUMBER;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_count
|
||||
FROM user_tables
|
||||
WHERE table_name = p_table_name;
|
||||
IF v_count = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20001,
|
||||
'Missing ' || p_table_name || '. Run 62_kb_aso_masking_backoffice_metadata.sql first.');
|
||||
END IF;
|
||||
END;
|
||||
BEGIN
|
||||
require_table('CB_MASKING_RULE');
|
||||
require_table('CB_COLUMN_MASKING_RULE');
|
||||
require_table('CB_USER_MASKING_RULE');
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === Updating trusted bearer context with column exception flags ===
|
||||
CREATE OR REPLACE PACKAGE cb_agent_ctx_pkg AUTHID DEFINER AS
|
||||
PROCEDURE clear_user;
|
||||
PROCEDURE set_user(p_user_id IN NUMBER);
|
||||
PROCEDURE set_user_values(
|
||||
p_user_id IN NUMBER,
|
||||
p_emp_no IN VARCHAR2,
|
||||
p_dept_code IN VARCHAR2,
|
||||
p_can_read_contents IN VARCHAR2
|
||||
);
|
||||
PROCEDURE set_user_by_bearer(p_bearer_key IN VARCHAR2);
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE PACKAGE BODY cb_agent_ctx_pkg AS
|
||||
PROCEDURE clear_stakeholder_context AS
|
||||
BEGIN
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID', NULL);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_ROLE', NULL);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL', NULL);
|
||||
END;
|
||||
|
||||
PROCEDURE clear_masking_rule_context AS
|
||||
BEGIN
|
||||
FOR r IN (SELECT column_id FROM cb_column_masking_rule) LOOP
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'MR_' || TO_CHAR(r.column_id), NULL);
|
||||
END LOOP;
|
||||
END;
|
||||
|
||||
PROCEDURE set_masking_rule_context(p_user_id IN NUMBER) AS
|
||||
BEGIN
|
||||
clear_masking_rule_context;
|
||||
FOR r IN (
|
||||
SELECT link.column_id,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_user_masking_rule user_rule
|
||||
WHERE user_rule.user_id = p_user_id
|
||||
AND user_rule.column_id = link.column_id
|
||||
AND user_rule.active_yn = 'Y'
|
||||
AND user_rule.decision = 'UNMASK'
|
||||
) THEN 'Y' ELSE 'N' END AS can_unmask
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
WHERE rule.enabled_yn = 'Y'
|
||||
) LOOP
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'MR_' || TO_CHAR(r.column_id), r.can_unmask);
|
||||
END LOOP;
|
||||
END;
|
||||
|
||||
PROCEDURE clear_user AS
|
||||
BEGIN
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'USER_ID', NULL);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'EMP_NO', NULL);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE', NULL);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'CAN_READ_CONTENTS', NULL);
|
||||
clear_stakeholder_context;
|
||||
clear_masking_rule_context;
|
||||
END;
|
||||
|
||||
PROCEDURE set_user_values(
|
||||
p_user_id IN NUMBER,
|
||||
p_emp_no IN VARCHAR2,
|
||||
p_dept_code IN VARCHAR2,
|
||||
p_can_read_contents IN VARCHAR2
|
||||
) AS
|
||||
BEGIN
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'USER_ID', TO_CHAR(p_user_id));
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'EMP_NO', p_emp_no);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE', p_dept_code);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'CAN_READ_CONTENTS', p_can_read_contents);
|
||||
clear_stakeholder_context;
|
||||
clear_masking_rule_context;
|
||||
END;
|
||||
|
||||
PROCEDURE set_stakeholder_context(
|
||||
p_stakeholder_user_id IN VARCHAR2,
|
||||
p_role IN VARCHAR2,
|
||||
p_channel IN VARCHAR2
|
||||
) AS
|
||||
v_channel VARCHAR2(50);
|
||||
BEGIN
|
||||
v_channel := CASE TRIM(p_channel)
|
||||
WHEN '설계사채널' THEN '설계사'
|
||||
WHEN 'GA채널' THEN 'GA'
|
||||
ELSE TRIM(p_channel)
|
||||
END;
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID', p_stakeholder_user_id);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_ROLE', p_role);
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL', v_channel);
|
||||
END;
|
||||
|
||||
PROCEDURE set_user(p_user_id IN NUMBER) AS
|
||||
v_emp_no cb_app_user.employee_no%TYPE;
|
||||
v_dept_code cb_app_user.dept_code%TYPE;
|
||||
v_can_read_contents cb_app_user.can_read_contents%TYPE;
|
||||
BEGIN
|
||||
SELECT employee_no,
|
||||
dept_code,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_user_role ur
|
||||
JOIN cb_permission p ON p.role_id = ur.role_id
|
||||
JOIN cb_permission_column pc ON pc.permission_id = p.perm_id
|
||||
WHERE ur.user_id = p_user_id
|
||||
AND p.target_name = 'CB_V_SEARCH_DOCUMENTS'
|
||||
AND p.action_name = 'SELECT'
|
||||
AND pc.column_name = 'CONTENTS'
|
||||
) THEN 'Y'
|
||||
ELSE 'N'
|
||||
END
|
||||
INTO v_emp_no, v_dept_code, v_can_read_contents
|
||||
FROM cb_app_user
|
||||
WHERE user_id = p_user_id
|
||||
AND active = 'Y';
|
||||
|
||||
set_user_values(p_user_id, v_emp_no, v_dept_code, v_can_read_contents);
|
||||
set_masking_rule_context(p_user_id);
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
clear_user;
|
||||
RAISE_APPLICATION_ERROR(-20003, 'Mapped application user not found');
|
||||
END;
|
||||
|
||||
PROCEDURE set_user_by_bearer(p_bearer_key IN VARCHAR2) AS
|
||||
v_user_id cb_app_user.user_id%TYPE;
|
||||
v_emp_no cb_app_user.employee_no%TYPE;
|
||||
v_dept_code cb_app_user.dept_code%TYPE;
|
||||
v_can_read_contents cb_app_user.can_read_contents%TYPE;
|
||||
v_stakeholder_user_id VARCHAR2(30);
|
||||
v_stakeholder_role VARCHAR2(50);
|
||||
v_stakeholder_channel VARCHAR2(50);
|
||||
BEGIN
|
||||
IF p_bearer_key IS NULL THEN
|
||||
clear_user;
|
||||
RAISE_APPLICATION_ERROR(-20001, 'Authorization Bearer key is required');
|
||||
END IF;
|
||||
|
||||
SELECT u.user_id,
|
||||
u.employee_no,
|
||||
u.dept_code,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_user_role ur
|
||||
JOIN cb_permission p ON p.role_id = ur.role_id
|
||||
JOIN cb_permission_column pc ON pc.permission_id = p.perm_id
|
||||
WHERE ur.user_id = u.user_id
|
||||
AND p.target_name = 'CB_V_SEARCH_DOCUMENTS'
|
||||
AND p.action_name = 'SELECT'
|
||||
AND pc.column_name = 'CONTENTS'
|
||||
) THEN 'Y'
|
||||
ELSE 'N'
|
||||
END,
|
||||
COALESCE(k.stakeholder_user_id, u.stakeholder_user_id)
|
||||
INTO v_user_id, v_emp_no, v_dept_code, v_can_read_contents, v_stakeholder_user_id
|
||||
FROM cb_agent_bearer_key k
|
||||
JOIN cb_app_user u ON u.user_id = k.user_id
|
||||
WHERE k.key_hash = STANDARD_HASH(p_bearer_key, 'SHA256')
|
||||
AND k.active = 'Y'
|
||||
AND k.revoked_at IS NULL
|
||||
AND (k.expires_at IS NULL OR k.expires_at > SYSDATE)
|
||||
AND u.active = 'Y';
|
||||
|
||||
set_user_values(v_user_id, v_emp_no, v_dept_code, v_can_read_contents);
|
||||
|
||||
IF v_stakeholder_user_id IS NOT NULL THEN
|
||||
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID', v_stakeholder_user_id);
|
||||
SELECT role, channel
|
||||
INTO v_stakeholder_role, v_stakeholder_channel
|
||||
FROM poc_2.kb_stakeholders
|
||||
WHERE user_id = v_stakeholder_user_id;
|
||||
set_stakeholder_context(v_stakeholder_user_id, v_stakeholder_role, v_stakeholder_channel);
|
||||
END IF;
|
||||
|
||||
set_masking_rule_context(v_user_id);
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
clear_user;
|
||||
RAISE_APPLICATION_ERROR(-20002, 'Invalid or expired Bearer key');
|
||||
END;
|
||||
END;
|
||||
/
|
||||
SHOW ERRORS PACKAGE BODY cb_agent_ctx_pkg
|
||||
|
||||
GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
|
||||
|
||||
PROMPT === Preserving the all-access admin raw-display contract ===
|
||||
MERGE INTO cb_user_masking_rule dst
|
||||
USING (
|
||||
SELECT user_row.user_id, column_rule.column_id
|
||||
FROM cb_app_user user_row
|
||||
CROSS JOIN cb_column_masking_rule column_rule
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = column_rule.rule_id
|
||||
WHERE user_row.stakeholder_user_id = 'KB_VPD_ADMIN'
|
||||
AND user_row.active = 'Y'
|
||||
AND rule.enabled_yn = 'Y'
|
||||
) src
|
||||
ON (dst.user_id = src.user_id AND dst.column_id = src.column_id)
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (user_id, column_id, decision, active_yn, updated_at)
|
||||
VALUES (src.user_id, src.column_id, 'UNMASK', 'Y', SYSTIMESTAMP);
|
||||
COMMIT;
|
||||
|
||||
PROMPT === Removing stale columns and disabling unconfigured KB policies ===
|
||||
-- A policy definition is deliberately retained when no column is configured.
|
||||
-- DISABLE_POLICY makes the effective state match the empty backoffice setting
|
||||
-- and lets a later column assignment re-enable the same named policy safely.
|
||||
DECLARE
|
||||
PROCEDURE reconcile_existing_policy(
|
||||
p_object_name IN VARCHAR2,
|
||||
p_policy_name IN VARCHAR2
|
||||
) IS
|
||||
v_policy_count NUMBER;
|
||||
v_configured_count NUMBER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_policy_count
|
||||
FROM redaction_policies
|
||||
WHERE object_owner = 'POC_2' AND object_name = p_object_name AND policy_name = p_policy_name;
|
||||
IF v_policy_count = 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO v_configured_count
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column column_row ON column_row.column_id = link.column_id
|
||||
JOIN cb_protected_object object_row ON object_row.object_id = column_row.object_id
|
||||
WHERE object_row.owner = 'POC_2'
|
||||
AND object_row.object_name = p_object_name
|
||||
AND rule.enabled_yn = 'Y';
|
||||
|
||||
IF v_configured_count = 0 THEN
|
||||
DBMS_REDACT.DISABLE_POLICY('POC_2', p_object_name, p_policy_name);
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
FOR stale_column IN (
|
||||
SELECT policy_column.column_name
|
||||
FROM redaction_columns policy_column
|
||||
WHERE policy_column.object_owner = 'POC_2'
|
||||
AND policy_column.object_name = p_object_name
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column column_row ON column_row.column_id = link.column_id
|
||||
JOIN cb_protected_object object_row ON object_row.object_id = column_row.object_id
|
||||
WHERE object_row.owner = 'POC_2'
|
||||
AND object_row.object_name = p_object_name
|
||||
AND column_row.column_name = policy_column.column_name
|
||||
AND rule.enabled_yn = 'Y'
|
||||
)
|
||||
) LOOP
|
||||
DBMS_REDACT.ALTER_POLICY(
|
||||
object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name,
|
||||
action => DBMS_REDACT.DROP_COLUMN, column_name => stale_column.column_name
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
BEGIN
|
||||
reconcile_existing_policy('KB_CUSTOMERS', 'KB_CUSTOMER_PII_REDACT');
|
||||
reconcile_existing_policy('KB_CLAIMS', 'KB_CLAIM_AMOUNT_REDACT');
|
||||
reconcile_existing_policy('KB_CONTRACTS', 'KB_CONTRACT_PREMIUM_REDACT');
|
||||
reconcile_existing_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_HOLDING_REDACT');
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === Integrating configured columns into the existing KB Data Redaction policies ===
|
||||
DECLARE
|
||||
v_policy_name VARCHAR2(128);
|
||||
v_expression_name VARCHAR2(128);
|
||||
v_expression VARCHAR2(4000);
|
||||
|
||||
PROCEDURE require_existing_policy(
|
||||
p_object_name IN VARCHAR2,
|
||||
p_policy_name IN VARCHAR2
|
||||
) IS
|
||||
v_count NUMBER;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_count
|
||||
FROM redaction_policies
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name = p_object_name
|
||||
AND policy_name = p_policy_name;
|
||||
IF v_count = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20012,
|
||||
'Existing Data Redaction policy not found: POC_2.' || p_object_name || '.' || p_policy_name);
|
||||
END IF;
|
||||
END;
|
||||
|
||||
PROCEDURE upsert_expression(
|
||||
p_expression_name IN VARCHAR2,
|
||||
p_expression IN VARCHAR2
|
||||
) IS
|
||||
v_count NUMBER;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_count
|
||||
FROM redaction_expressions
|
||||
WHERE policy_expression_name = p_expression_name;
|
||||
IF v_count = 0 THEN
|
||||
DBMS_REDACT.CREATE_POLICY_EXPRESSION(
|
||||
policy_expression_name => p_expression_name,
|
||||
expression => p_expression,
|
||||
policy_expression_description => 'Mask unless trusted bearer context grants an original-value exception'
|
||||
);
|
||||
ELSE
|
||||
DBMS_REDACT.UPDATE_POLICY_EXPRESSION(
|
||||
policy_expression_name => p_expression_name,
|
||||
expression => p_expression,
|
||||
policy_expression_description => 'Mask unless trusted bearer context grants an original-value exception'
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
|
||||
PROCEDURE modify_column(
|
||||
p_object_name IN VARCHAR2,
|
||||
p_policy_name IN VARCHAR2,
|
||||
p_column_name IN VARCHAR2,
|
||||
p_template_code IN VARCHAR2
|
||||
) IS
|
||||
PROCEDURE apply(
|
||||
p_function_type IN BINARY_INTEGER,
|
||||
p_pattern IN VARCHAR2 DEFAULT NULL,
|
||||
p_replace IN VARCHAR2 DEFAULT NULL
|
||||
) IS
|
||||
BEGIN
|
||||
DBMS_REDACT.ALTER_POLICY(
|
||||
object_schema => 'POC_2',
|
||||
object_name => p_object_name,
|
||||
policy_name => p_policy_name,
|
||||
action => DBMS_REDACT.MODIFY_COLUMN,
|
||||
column_name => p_column_name,
|
||||
function_type => p_function_type,
|
||||
regexp_pattern => p_pattern,
|
||||
regexp_replace_string => p_replace
|
||||
);
|
||||
END;
|
||||
BEGIN
|
||||
CASE p_template_code
|
||||
WHEN 'NULLIFY' THEN
|
||||
apply(DBMS_REDACT.NULLIFY);
|
||||
WHEN 'FULL' THEN
|
||||
apply(DBMS_REDACT.FULL);
|
||||
WHEN 'TEXT_PARTIAL' THEN
|
||||
apply(DBMS_REDACT.REGEXP, '(^.).*$', '\1***');
|
||||
WHEN 'RRN_PARTIAL' THEN
|
||||
apply(DBMS_REDACT.REGEXP, '(^[0-9]{6})-?[0-9]{7}$', '\1-*******');
|
||||
ELSE
|
||||
RAISE_APPLICATION_ERROR(-20013, 'Unsupported masking template: ' || p_template_code);
|
||||
END CASE;
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
FOR configured_column IN (
|
||||
SELECT protected_object.object_name,
|
||||
protected_column.column_id,
|
||||
protected_column.column_name,
|
||||
masking_rule.template_code
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule masking_rule ON masking_rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column protected_column ON protected_column.column_id = link.column_id
|
||||
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||
WHERE protected_object.owner = 'POC_2'
|
||||
AND masking_rule.enabled_yn = 'Y'
|
||||
ORDER BY protected_object.object_name, protected_column.column_id
|
||||
) LOOP
|
||||
v_policy_name := CASE configured_column.object_name
|
||||
WHEN 'KB_CUSTOMERS' THEN 'KB_CUSTOMER_PII_REDACT'
|
||||
WHEN 'KB_CLAIMS' THEN 'KB_CLAIM_AMOUNT_REDACT'
|
||||
WHEN 'KB_CONTRACTS' THEN 'KB_CONTRACT_PREMIUM_REDACT'
|
||||
WHEN 'KB_EXTERNAL_HOLDINGS' THEN 'KB_EXT_HOLDING_REDACT'
|
||||
ELSE NULL
|
||||
END;
|
||||
IF v_policy_name IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20014,
|
||||
'No existing KB Data Redaction policy mapping for ' || configured_column.object_name);
|
||||
END IF;
|
||||
|
||||
require_existing_policy(configured_column.object_name, v_policy_name);
|
||||
DBMS_REDACT.ENABLE_POLICY('POC_2', configured_column.object_name, v_policy_name);
|
||||
modify_column(
|
||||
configured_column.object_name,
|
||||
v_policy_name,
|
||||
configured_column.column_name,
|
||||
configured_column.template_code
|
||||
);
|
||||
|
||||
v_expression_name := 'CBMR_' || TO_CHAR(configured_column.column_id);
|
||||
v_expression := 'SYS_CONTEXT(''CB_AGENT_CTX'', ''MR_' || TO_CHAR(configured_column.column_id)
|
||||
|| ''') IS NULL OR SYS_CONTEXT(''CB_AGENT_CTX'', ''MR_' || TO_CHAR(configured_column.column_id) || ''') <> ''Y''';
|
||||
upsert_expression(v_expression_name, v_expression);
|
||||
DBMS_REDACT.APPLY_POLICY_EXPR_TO_COL(
|
||||
object_schema => 'POC_2',
|
||||
object_name => configured_column.object_name,
|
||||
column_name => configured_column.column_name,
|
||||
policy_expression_name => v_expression_name
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
/
|
||||
COMMIT;
|
||||
|
||||
PROMPT === Verification ===
|
||||
SELECT object_name, policy_name, expression
|
||||
FROM redaction_policies
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name IN ('KB_CUSTOMERS', 'KB_CLAIMS', 'KB_CONTRACTS', 'KB_EXTERNAL_HOLDINGS')
|
||||
ORDER BY object_name, policy_name;
|
||||
|
||||
SELECT object_name, column_name, function_type
|
||||
FROM redaction_columns
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name IN ('KB_CUSTOMERS', 'KB_CLAIMS', 'KB_CONTRACTS', 'KB_EXTERNAL_HOLDINGS')
|
||||
ORDER BY object_name, column_name;
|
||||
|
||||
SELECT policy_expression_name, object_name, column_name, expression
|
||||
FROM redaction_expressions
|
||||
WHERE policy_expression_name LIKE 'CBMR_%'
|
||||
ORDER BY policy_expression_name;
|
||||
|
||||
SELECT user_row.user_name,
|
||||
protected_object.object_name,
|
||||
protected_column.column_name,
|
||||
user_rule.decision
|
||||
FROM cb_user_masking_rule user_rule
|
||||
JOIN cb_app_user user_row ON user_row.user_id = user_rule.user_id
|
||||
JOIN cb_protected_column protected_column ON protected_column.column_id = user_rule.column_id
|
||||
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||
ORDER BY user_row.user_name, protected_object.object_name, protected_column.column_name;
|
||||
EXIT
|
||||
|
||||
-- Legacy replacement implementation retained below for migration reference.
|
||||
-- SQLcl exits above, so it never executes.
|
||||
PROMPT === Rebuilding managed Oracle Data Redaction policies ===
|
||||
DECLARE
|
||||
v_policy_name VARCHAR2(128);
|
||||
v_expression_name VARCHAR2(128);
|
||||
v_first BOOLEAN;
|
||||
|
||||
PROCEDURE apply_column(
|
||||
p_first IN BOOLEAN,
|
||||
p_object_name IN VARCHAR2,
|
||||
p_policy_name IN VARCHAR2,
|
||||
p_column_name IN VARCHAR2,
|
||||
p_template_code IN VARCHAR2
|
||||
) IS
|
||||
PROCEDURE first_policy(
|
||||
p_function_type IN BINARY_INTEGER,
|
||||
p_pattern IN VARCHAR2 DEFAULT NULL,
|
||||
p_replace IN VARCHAR2 DEFAULT NULL
|
||||
) IS
|
||||
BEGIN
|
||||
DBMS_REDACT.ADD_POLICY(
|
||||
object_schema => 'POC_2',
|
||||
object_name => p_object_name,
|
||||
policy_name => p_policy_name,
|
||||
policy_description => 'Managed by CB masking rule backoffice',
|
||||
column_name => p_column_name,
|
||||
function_type => p_function_type,
|
||||
expression => '1=1',
|
||||
regexp_pattern => p_pattern,
|
||||
regexp_replace_string => p_replace,
|
||||
enable => TRUE
|
||||
);
|
||||
END;
|
||||
|
||||
PROCEDURE additional_column(
|
||||
p_function_type IN BINARY_INTEGER,
|
||||
p_pattern IN VARCHAR2 DEFAULT NULL,
|
||||
p_replace IN VARCHAR2 DEFAULT NULL
|
||||
) IS
|
||||
BEGIN
|
||||
DBMS_REDACT.ALTER_POLICY(
|
||||
object_schema => 'POC_2',
|
||||
object_name => p_object_name,
|
||||
policy_name => p_policy_name,
|
||||
action => DBMS_REDACT.ADD_COLUMN,
|
||||
column_name => p_column_name,
|
||||
function_type => p_function_type,
|
||||
regexp_pattern => p_pattern,
|
||||
regexp_replace_string => p_replace
|
||||
);
|
||||
END;
|
||||
|
||||
PROCEDURE invoke(
|
||||
p_function_type IN BINARY_INTEGER,
|
||||
p_pattern IN VARCHAR2 DEFAULT NULL,
|
||||
p_replace IN VARCHAR2 DEFAULT NULL
|
||||
) IS
|
||||
BEGIN
|
||||
IF p_first THEN
|
||||
first_policy(p_function_type, p_pattern, p_replace);
|
||||
ELSE
|
||||
additional_column(p_function_type, p_pattern, p_replace);
|
||||
END IF;
|
||||
END;
|
||||
BEGIN
|
||||
CASE p_template_code
|
||||
WHEN 'NULLIFY' THEN
|
||||
invoke(DBMS_REDACT.NULLIFY);
|
||||
WHEN 'FULL' THEN
|
||||
invoke(DBMS_REDACT.FULL);
|
||||
WHEN 'TEXT_PARTIAL' THEN
|
||||
invoke(DBMS_REDACT.REGEXP, '(^.).*$', '\1***');
|
||||
WHEN 'RRN_PARTIAL' THEN
|
||||
invoke(DBMS_REDACT.REGEXP, '(^[0-9]{6})-?[0-9]{7}$', '\1-*******');
|
||||
ELSE
|
||||
RAISE_APPLICATION_ERROR(-20011, 'Unsupported template: ' || p_template_code);
|
||||
END CASE;
|
||||
END;
|
||||
|
||||
PROCEDURE drop_managed_policy(p_object_name IN VARCHAR2, p_policy_name IN VARCHAR2) IS
|
||||
BEGIN
|
||||
DBMS_REDACT.DROP_POLICY(
|
||||
object_schema => 'POC_2',
|
||||
object_name => p_object_name,
|
||||
policy_name => p_policy_name
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
PROCEDURE drop_expression(p_expression_name IN VARCHAR2) IS
|
||||
BEGIN
|
||||
DBMS_REDACT.DROP_POLICY_EXPRESSION(p_expression_name);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
-- These three policies are legacy test policies found on the KB objects.
|
||||
-- Data Redaction permits only one policy per object, so replace them with
|
||||
-- the managed policy rather than leaving their client-identifier tests live.
|
||||
drop_managed_policy('KB_CUSTOMERS', 'KB_CUSTOMER_PII_REDACT');
|
||||
drop_managed_policy('KB_CLAIMS', 'KB_CLAIM_AMOUNT_REDACT');
|
||||
drop_managed_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_HOLDING_REDACT');
|
||||
|
||||
FOR object_row IN (
|
||||
SELECT DISTINCT po.object_name
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column pc ON pc.column_id = link.column_id
|
||||
JOIN cb_protected_object po ON po.object_id = pc.object_id
|
||||
WHERE rule.enabled_yn = 'Y'
|
||||
AND po.owner = 'POC_2'
|
||||
) LOOP
|
||||
v_policy_name := 'CB_ASO_' || object_row.object_name || '_MASK';
|
||||
drop_managed_policy(object_row.object_name, v_policy_name);
|
||||
END LOOP;
|
||||
|
||||
FOR column_row IN (
|
||||
SELECT link.column_id
|
||||
FROM cb_column_masking_rule link
|
||||
) LOOP
|
||||
drop_expression('CBMR_' || TO_CHAR(column_row.column_id));
|
||||
END LOOP;
|
||||
|
||||
FOR object_row IN (
|
||||
SELECT DISTINCT po.object_name
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column pc ON pc.column_id = link.column_id
|
||||
JOIN cb_protected_object po ON po.object_id = pc.object_id
|
||||
WHERE rule.enabled_yn = 'Y'
|
||||
AND po.owner = 'POC_2'
|
||||
ORDER BY po.object_name
|
||||
) LOOP
|
||||
v_policy_name := 'CB_ASO_' || object_row.object_name || '_MASK';
|
||||
v_first := TRUE;
|
||||
FOR column_row IN (
|
||||
SELECT pc.column_id,
|
||||
pc.column_name,
|
||||
rule.template_code,
|
||||
rule.rule_name
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column pc ON pc.column_id = link.column_id
|
||||
JOIN cb_protected_object po ON po.object_id = pc.object_id
|
||||
WHERE po.owner = 'POC_2'
|
||||
AND po.object_name = object_row.object_name
|
||||
AND rule.enabled_yn = 'Y'
|
||||
ORDER BY pc.column_id
|
||||
) LOOP
|
||||
apply_column(v_first, object_row.object_name, v_policy_name,
|
||||
column_row.column_name, column_row.template_code);
|
||||
v_expression_name := 'CBMR_' || TO_CHAR(column_row.column_id);
|
||||
DBMS_REDACT.CREATE_POLICY_EXPRESSION(
|
||||
policy_expression_name => v_expression_name,
|
||||
expression => 'SYS_CONTEXT(''CB_AGENT_CTX'', ''MR_' || TO_CHAR(column_row.column_id)
|
||||
|| ''') IS NULL OR SYS_CONTEXT(''CB_AGENT_CTX'', ''MR_' || TO_CHAR(column_row.column_id) || ''') <> ''Y''',
|
||||
policy_expression_description => 'Mask unless trusted bearer context grants an original-value exception'
|
||||
);
|
||||
DBMS_REDACT.APPLY_POLICY_EXPR_TO_COL(
|
||||
object_schema => 'POC_2',
|
||||
object_name => object_row.object_name,
|
||||
column_name => column_row.column_name,
|
||||
policy_expression_name => v_expression_name
|
||||
);
|
||||
v_first := FALSE;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END;
|
||||
/
|
||||
COMMIT;
|
||||
|
||||
PROMPT === Verification ===
|
||||
SELECT object_name, policy_name, expression
|
||||
FROM redaction_policies
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND policy_name LIKE 'CB_ASO_%'
|
||||
ORDER BY object_name, policy_name;
|
||||
|
||||
SELECT object_name, column_name, function_type
|
||||
FROM redaction_columns
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name IN ('KB_CUSTOMERS', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
|
||||
ORDER BY object_name, column_name;
|
||||
|
||||
SELECT user_row.user_name,
|
||||
protected_object.object_name,
|
||||
protected_column.column_name,
|
||||
user_rule.decision
|
||||
FROM cb_user_masking_rule user_rule
|
||||
JOIN cb_app_user user_row ON user_row.user_id = user_rule.user_id
|
||||
JOIN cb_protected_column protected_column ON protected_column.column_id = user_rule.column_id
|
||||
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||
ORDER BY user_row.user_name, protected_object.object_name, protected_column.column_name;
|
||||
EXIT
|
||||
54
sql/adb/64_kb_aso_masking_default_column_rules.sql
Normal file
@@ -0,0 +1,54 @@
|
||||
-- ============================================================
|
||||
-- 64_kb_aso_masking_default_column_rules.sql
|
||||
--
|
||||
-- Initial blacklist of KB columns subject to ASO masking.
|
||||
-- Run as ADMIN after 62. Execute 63 afterwards to apply the
|
||||
-- configured rules as DBMS_REDACT policies.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET DEFINE OFF
|
||||
SET FEEDBACK ON
|
||||
|
||||
PROMPT === Assigning the KB masking target blacklist ===
|
||||
MERGE INTO cb_column_masking_rule dst
|
||||
USING (
|
||||
SELECT protected_column.column_id,
|
||||
masking_rule.rule_id
|
||||
FROM cb_protected_column protected_column
|
||||
JOIN cb_protected_object protected_object
|
||||
ON protected_object.object_id = protected_column.object_id
|
||||
JOIN cb_masking_rule masking_rule
|
||||
ON masking_rule.rule_code = CASE
|
||||
WHEN protected_object.object_name = 'KB_CUSTOMERS'
|
||||
AND protected_column.column_name = 'RRN_MASKED'
|
||||
THEN 'MASK_RRN_PARTIAL'
|
||||
ELSE 'MASK_NULLIFY'
|
||||
END
|
||||
WHERE (protected_object.object_name = 'KB_CUSTOMERS'
|
||||
AND protected_column.column_name IN ('RRN_MASKED'))
|
||||
OR (protected_object.object_name = 'KB_CLAIMS'
|
||||
AND protected_column.column_name IN ('CLAIM_AMT', 'PAID_AMT'))
|
||||
OR (protected_object.object_name = 'KB_EXTERNAL_HOLDINGS'
|
||||
AND protected_column.column_name IN ('EXT_INSURER', 'EXT_PRODUCT_GRP', 'EXT_PRODUCT_TYPE'))
|
||||
) src
|
||||
ON (dst.column_id = src.column_id)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET dst.rule_id = src.rule_id, dst.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (column_id, rule_id, updated_at)
|
||||
VALUES (src.column_id, src.rule_id, SYSTIMESTAMP);
|
||||
COMMIT;
|
||||
|
||||
PROMPT === Configured masking target blacklist ===
|
||||
SELECT protected_object.owner,
|
||||
protected_object.object_name,
|
||||
protected_column.column_name,
|
||||
masking_rule.rule_code,
|
||||
masking_rule.rule_name,
|
||||
masking_rule.template_code
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_protected_column protected_column ON protected_column.column_id = link.column_id
|
||||
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||
JOIN cb_masking_rule masking_rule ON masking_rule.rule_id = link.rule_id
|
||||
ORDER BY protected_object.object_name, protected_column.column_name;
|
||||
EXIT
|
||||
221
sql/adb/65_kb_select_ai_vpd_query_api.sql
Normal file
@@ -0,0 +1,221 @@
|
||||
-- ============================================================
|
||||
-- 65_kb_select_ai_vpd_query_api.sql
|
||||
--
|
||||
-- Natural-language KB query API backing package.
|
||||
-- Run as POC_2. The caller (CB_ORDS) establishes CB_AGENT_CTX before
|
||||
-- invoking this package, so generated SQL is still subject to POC_2 VPD.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET DEFINE OFF
|
||||
SET FEEDBACK ON
|
||||
|
||||
PROMPT === Annotating KB schema for Select AI ===
|
||||
COMMENT ON TABLE poc_2.kb_customers IS '고객원장. CUST_ID는 C로 시작하는 고객 식별자 예: C1001006.';
|
||||
COMMENT ON COLUMN poc_2.kb_customers.cust_id IS '고객 식별자. C로 시작하는 값 예: C1001006.';
|
||||
|
||||
COMMENT ON TABLE poc_2.kb_products IS '상품원장 및 약관상품 마스터. PRODUCT_CD는 숫자형 상품/약관 코드 예: 41048.';
|
||||
COMMENT ON COLUMN poc_2.kb_products.product_cd IS '상품 또는 약관상품 코드. 숫자형 코드 예: 41048. KB_CONTRACTS.PRODUCT_CD와 조인한다.';
|
||||
COMMENT ON COLUMN poc_2.kb_products.clause_product_nm IS '약관상품명 또는 상품명. 고객 설명용 상품 비교에 사용한다.';
|
||||
COMMENT ON COLUMN poc_2.kb_products.insurance_type IS '보험 대분류. 예: 자동차, 장기, 일반.';
|
||||
COMMENT ON COLUMN poc_2.kb_products.product_type IS '상품 유형 또는 세부 상품군.';
|
||||
COMMENT ON COLUMN poc_2.kb_products.clause_version IS '약관 버전.';
|
||||
COMMENT ON COLUMN poc_2.kb_products.sale_status IS '판매 상태.';
|
||||
COMMENT ON COLUMN poc_2.kb_products.active_yn IS '상품 활성 여부.';
|
||||
|
||||
COMMENT ON TABLE poc_2.kb_contracts IS '계약원장. CONTRACT_NO는 CT로 시작하는 계약번호이고, PRODUCT_CD는 숫자형 상품/약관 코드다.';
|
||||
COMMENT ON COLUMN poc_2.kb_contracts.contract_no IS '계약번호. CT로 시작하는 값 예: CT2699001. 41048 같은 순수 숫자 코드는 계약번호가 아니라 PRODUCT_CD일 가능성이 높다.';
|
||||
COMMENT ON COLUMN poc_2.kb_contracts.cust_id IS '계약 고객 식별자. KB_CUSTOMERS.CUST_ID와 조인한다.';
|
||||
COMMENT ON COLUMN poc_2.kb_contracts.product_cd IS '계약의 상품/약관 코드. 숫자형 코드 예: 41048. KB_PRODUCTS.PRODUCT_CD와 조인한다.';
|
||||
COMMENT ON COLUMN poc_2.kb_contracts.fc_id IS '담당 설계사 또는 FC 사용자 ID. 예: FC00789.';
|
||||
COMMENT ON COLUMN poc_2.kb_contracts.fc_channel IS '담당 채널. 예: 설계사, GA, 다이렉트.';
|
||||
COMMENT ON COLUMN poc_2.kb_contracts.contract_status IS '계약 상태. 예: 정상, 실효.';
|
||||
COMMENT ON COLUMN poc_2.kb_contracts.premium IS '계약 보험료.';
|
||||
COMMENT ON COLUMN poc_2.kb_contracts.pay_cycle IS '보험료 납입 주기.';
|
||||
|
||||
COMMENT ON TABLE poc_2.kb_coverages IS '담보원장. 계약번호 CONTRACT_NO 기준으로 KB_CONTRACTS와 조인해 보장/특약을 설명한다.';
|
||||
COMMENT ON COLUMN poc_2.kb_coverages.contract_no IS '담보가 속한 계약번호. KB_CONTRACTS.CONTRACT_NO와 조인한다.';
|
||||
COMMENT ON COLUMN poc_2.kb_coverages.cust_id IS '담보 고객 식별자.';
|
||||
COMMENT ON COLUMN poc_2.kb_coverages.coverage_nm IS '담보명 또는 특약명.';
|
||||
COMMENT ON COLUMN poc_2.kb_coverages.coverage_type IS '담보 유형.';
|
||||
COMMENT ON COLUMN poc_2.kb_coverages.coverage_div IS '담보 구분.';
|
||||
COMMENT ON COLUMN poc_2.kb_coverages.insured_amt IS '가입금액 또는 보장금액.';
|
||||
COMMENT ON COLUMN poc_2.kb_coverages.renew_due_dt IS '갱신 예정일.';
|
||||
|
||||
COMMENT ON TABLE poc_2.kb_external_holdings IS '외부보유정보 원장. 타 보험사 보유계약과 약관상품을 고객 CUST_ID 기준으로 비교한다.';
|
||||
COMMENT ON COLUMN poc_2.kb_external_holdings.cust_id IS '외부보유정보 고객 식별자. KB_CUSTOMERS.CUST_ID와 조인한다.';
|
||||
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_insurer IS '외부 보험사명. 예: 삼성화재.';
|
||||
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_product_grp IS '외부 상품군. 예: 자동차.';
|
||||
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_product_type IS '외부 상품 유형. 예: 개인용.';
|
||||
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_clause_nm IS '외부 약관상품명.';
|
||||
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_sale_status IS '외부 상품 판매 상태.';
|
||||
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_renew_month IS '외부 보유계약 갱신월.';
|
||||
|
||||
PROMPT === Creating VPD-aware Select AI query API ===
|
||||
CREATE OR REPLACE PACKAGE poc_2.kb_select_ai_vpd_query_api AUTHID DEFINER AS
|
||||
PROCEDURE open_query(
|
||||
p_prompt IN CLOB,
|
||||
p_limit IN PLS_INTEGER,
|
||||
p_rows OUT SYS_REFCURSOR,
|
||||
p_generated_sql OUT CLOB
|
||||
);
|
||||
END kb_select_ai_vpd_query_api;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE PACKAGE BODY poc_2.kb_select_ai_vpd_query_api AS
|
||||
c_profile_name CONSTANT VARCHAR2(128) := 'KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1';
|
||||
c_max_prompt_length CONSTANT PLS_INTEGER := 4000;
|
||||
c_max_rows CONSTANT PLS_INTEGER := 100;
|
||||
|
||||
FUNCTION normalized_sql(p_generated_sql IN CLOB) RETURN VARCHAR2 IS
|
||||
v_sql VARCHAR2(32767);
|
||||
v_comment_end PLS_INTEGER;
|
||||
BEGIN
|
||||
IF p_generated_sql IS NULL OR DBMS_LOB.GETLENGTH(p_generated_sql) = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20811, 'Select AI did not generate SQL.');
|
||||
END IF;
|
||||
IF DBMS_LOB.GETLENGTH(p_generated_sql) > 32767 THEN
|
||||
RAISE_APPLICATION_ERROR(-20812, 'Generated SQL is too long.');
|
||||
END IF;
|
||||
|
||||
v_sql := TRIM(DBMS_LOB.SUBSTR(p_generated_sql, 32767, 1));
|
||||
|
||||
-- Select AI sometimes wraps an otherwise valid SQL statement in markdown
|
||||
-- fences or adds a short leading explanation comment. Strip only those
|
||||
-- wrapper artifacts before the strict safety validator runs. Comments that
|
||||
-- remain inside the executable statement are still rejected below.
|
||||
LOOP
|
||||
v_sql := TRIM(v_sql);
|
||||
v_sql := REGEXP_REPLACE(v_sql, '^[[:space:]]*```[[:alpha:]]*[[:space:]]*', '');
|
||||
v_sql := REGEXP_REPLACE(v_sql, '[[:space:]]*```[[:space:]]*$', '');
|
||||
v_sql := TRIM(v_sql);
|
||||
|
||||
IF SUBSTR(v_sql, 1, 2) = '--' THEN
|
||||
v_comment_end := INSTR(v_sql, CHR(10));
|
||||
IF v_comment_end = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20813, 'Select AI returned only a comment, not SQL.');
|
||||
END IF;
|
||||
v_sql := SUBSTR(v_sql, v_comment_end + 1);
|
||||
ELSIF SUBSTR(v_sql, 1, 2) = '/*' THEN
|
||||
v_comment_end := INSTR(v_sql, '*/');
|
||||
IF v_comment_end = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20814, 'Unclosed generated SQL comment is not allowed.');
|
||||
END IF;
|
||||
v_sql := SUBSTR(v_sql, v_comment_end + 2);
|
||||
ELSE
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
v_sql := RTRIM(TRIM(v_sql), ';');
|
||||
RETURN TRIM(v_sql);
|
||||
END normalized_sql;
|
||||
|
||||
PROCEDURE assert_safe_kb_select(p_sql IN VARCHAR2) IS
|
||||
-- Select AI normally double-quotes Oracle identifiers. Validate a quote-free
|
||||
-- copy so POC_2.KB_CLAIMS and "POC_2"."KB_CLAIMS" follow the same policy.
|
||||
v_upper VARCHAR2(32767) := REPLACE(UPPER(p_sql), '"', '');
|
||||
-- Mask string literals before checking semicolons, keywords, packages, and
|
||||
-- data dictionary names. A harmless CASE output string should not be treated
|
||||
-- as executable SQL syntax.
|
||||
v_lexical VARCHAR2(32767) :=
|
||||
REGEXP_REPLACE(REPLACE(UPPER(p_sql), '"', ''), '''(''''|[^''])*''', '''X''');
|
||||
BEGIN
|
||||
IF NOT REGEXP_LIKE(v_lexical, '^(SELECT|WITH)[[:space:]]') THEN
|
||||
RAISE_APPLICATION_ERROR(
|
||||
-20813,
|
||||
'Only a single SELECT or WITH query is allowed. Generated prefix: '
|
||||
|| SUBSTR(v_upper, 1, 160)
|
||||
);
|
||||
END IF;
|
||||
IF INSTR(v_lexical, ';') > 0
|
||||
OR INSTR(v_lexical, '--') > 0
|
||||
OR INSTR(v_lexical, '/*') > 0
|
||||
OR INSTR(v_lexical, '*/') > 0 THEN
|
||||
RAISE_APPLICATION_ERROR(
|
||||
-20814,
|
||||
'Comments or multiple SQL statements remain after normalization. Generated prefix: '
|
||||
|| SUBSTR(v_upper, 1, 160)
|
||||
);
|
||||
END IF;
|
||||
IF REGEXP_LIKE(
|
||||
v_lexical,
|
||||
'(^|[^A-Z_])(ALTER|BEGIN|COMMIT|CREATE|DECLARE|DELETE|DROP|EXECUTE|GRANT|INSERT|MERGE|REVOKE|ROLLBACK|TRUNCATE|UPDATE)([^A-Z_]|$)'
|
||||
) THEN
|
||||
RAISE_APPLICATION_ERROR(-20815, 'Only read-only SQL is allowed.');
|
||||
END IF;
|
||||
IF INSTR(v_lexical, 'DBMS_') > 0
|
||||
OR INSTR(v_lexical, 'UTL_') > 0
|
||||
OR INSTR(v_lexical, 'SYS.') > 0
|
||||
OR INSTR(v_lexical, 'ADMIN.') > 0
|
||||
OR INSTR(v_lexical, 'CB_') > 0
|
||||
OR INSTR(v_lexical, 'ALL_') > 0
|
||||
OR INSTR(v_lexical, 'DBA_') > 0
|
||||
OR INSTR(v_lexical, 'USER_') > 0
|
||||
OR INSTR(v_lexical, 'FOR UPDATE') > 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20816, 'System, backoffice, and locking objects are not allowed.');
|
||||
END IF;
|
||||
IF NOT REGEXP_LIKE(
|
||||
v_lexical,
|
||||
'(FROM|JOIN)[[:space:]]+(POC_2[.])?KB_(CUSTOMERS|PRODUCTS|CONTRACTS|COVERAGES|CLAIMS|EXTERNAL_HOLDINGS|STAKEHOLDERS)([[:space:],)]|$)'
|
||||
) THEN
|
||||
RAISE_APPLICATION_ERROR(-20817, 'The generated SQL must query a permitted KB business table.');
|
||||
END IF;
|
||||
END assert_safe_kb_select;
|
||||
|
||||
PROCEDURE open_query(
|
||||
p_prompt IN CLOB,
|
||||
p_limit IN PLS_INTEGER,
|
||||
p_rows OUT SYS_REFCURSOR,
|
||||
p_generated_sql OUT CLOB
|
||||
) IS
|
||||
v_prompt VARCHAR2(4000);
|
||||
v_generation_prompt CLOB;
|
||||
v_generated_sql CLOB;
|
||||
v_sql VARCHAR2(32767);
|
||||
v_limit PLS_INTEGER;
|
||||
BEGIN
|
||||
IF p_prompt IS NULL OR DBMS_LOB.GETLENGTH(TRIM(p_prompt)) = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20801, 'prompt is required.');
|
||||
END IF;
|
||||
IF DBMS_LOB.GETLENGTH(p_prompt) > c_max_prompt_length THEN
|
||||
RAISE_APPLICATION_ERROR(-20802, 'prompt must be 4,000 characters or fewer.');
|
||||
END IF;
|
||||
|
||||
v_prompt := TRIM(DBMS_LOB.SUBSTR(p_prompt, c_max_prompt_length, 1));
|
||||
IF REGEXP_LIKE(UPPER(v_prompt), 'SELECT[[:space:]]+AI') THEN
|
||||
RAISE_APPLICATION_ERROR(-20803, 'SELECT AI action prefixes are not allowed in prompt.');
|
||||
END IF;
|
||||
v_limit := LEAST(GREATEST(NVL(p_limit, 50), 1), c_max_rows);
|
||||
|
||||
v_generation_prompt :=
|
||||
'Return exactly one Oracle SQL SELECT or WITH statement and no markdown. '
|
||||
|| 'Use only these POC_2 business tables: KB_CUSTOMERS, KB_PRODUCTS, KB_CONTRACTS, '
|
||||
|| 'KB_COVERAGES, KB_CLAIMS, KB_EXTERNAL_HOLDINGS, KB_STAKEHOLDERS. '
|
||||
|| 'Never use DDL, DML, PL/SQL, system views, backoffice tables, comments, or FOR UPDATE. '
|
||||
|| 'The caller request is: ' || v_prompt;
|
||||
|
||||
v_generated_sql := DBMS_CLOUD_AI.GENERATE(
|
||||
prompt => v_generation_prompt,
|
||||
profile_name => c_profile_name,
|
||||
action => 'showsql'
|
||||
);
|
||||
v_sql := normalized_sql(v_generated_sql);
|
||||
assert_safe_kb_select(v_sql);
|
||||
|
||||
-- CB_AGENT_CTX was set by CB_ORDS before this call. Parsing this statement
|
||||
-- in the same database session makes Oracle evaluate POC_2 VPD policies.
|
||||
OPEN p_rows FOR
|
||||
'SELECT * FROM (' || v_sql || ') WHERE ROWNUM <= :row_limit'
|
||||
USING v_limit;
|
||||
p_generated_sql := v_sql;
|
||||
END open_query;
|
||||
END kb_select_ai_vpd_query_api;
|
||||
/
|
||||
|
||||
SHOW ERRORS
|
||||
|
||||
PROMPT === Granting fixed query API to CB_ORDS ===
|
||||
GRANT EXECUTE ON poc_2.kb_select_ai_vpd_query_api TO cb_ords;
|
||||
|
||||
PROMPT === POC_2 Select AI VPD query API ready ===
|
||||
EXIT
|
||||
149
sql/adb/66_kb_select_ai_vpd_query_ords.sql
Normal file
@@ -0,0 +1,149 @@
|
||||
-- ============================================================
|
||||
-- 66_kb_select_ai_vpd_query_ords.sql
|
||||
--
|
||||
-- POST /ords/cb-ords/kb-select-ai-vpd/query
|
||||
-- Authorization: Bearer <VPD token>
|
||||
-- {"prompt":"채널별 청구 건수를 보여줘", "limit":50}
|
||||
--
|
||||
-- Run as CB_ORDS after 65. The Bearer token is resolved before Select AI
|
||||
-- generates and executes its single validated read-only SQL statement.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET DEFINE OFF
|
||||
SET FEEDBACK ON
|
||||
|
||||
PROMPT === Granting context setup to the ORDS gateway ===
|
||||
GRANT EXECUTE ON cb_ords_handler_pkg TO ords_public_user;
|
||||
|
||||
PROMPT === Resetting only the VPD Select AI query module ===
|
||||
BEGIN
|
||||
ORDS.DELETE_MODULE(p_module_name => 'kb.select.ai.vpd');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === Creating VPD-aware Select AI query endpoint ===
|
||||
BEGIN
|
||||
ORDS.DEFINE_MODULE(
|
||||
p_module_name => 'kb.select.ai.vpd',
|
||||
p_base_path => 'kb-select-ai-vpd/',
|
||||
p_items_per_page => 0,
|
||||
p_status => 'PUBLISHED'
|
||||
);
|
||||
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'kb.select.ai.vpd',
|
||||
p_pattern => 'query'
|
||||
);
|
||||
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'kb.select.ai.vpd',
|
||||
p_pattern => 'query',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.source_type_plsql,
|
||||
p_source => q'~
|
||||
DECLARE
|
||||
v_body_text CLOB;
|
||||
v_prompt VARCHAR2(4000);
|
||||
v_limit PLS_INTEGER;
|
||||
v_rows SYS_REFCURSOR;
|
||||
v_generated_sql CLOB;
|
||||
v_response CLOB;
|
||||
v_error_code NUMBER;
|
||||
v_error_message VARCHAR2(4000);
|
||||
BEGIN
|
||||
-- Resolves the Bearer token and sets the VPD/ASO application context in
|
||||
-- this very ORDS database session before the generated SQL is parsed.
|
||||
cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id);
|
||||
|
||||
v_body_text := :body_text;
|
||||
v_prompt := JSON_VALUE(v_body_text, '$.prompt' RETURNING VARCHAR2(4000) NULL ON ERROR);
|
||||
v_limit := JSON_VALUE(v_body_text, '$.limit' RETURNING NUMBER DEFAULT 50 ON ERROR);
|
||||
|
||||
IF v_prompt IS NULL OR TRIM(v_prompt) IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20801, 'prompt is required');
|
||||
END IF;
|
||||
IF v_limit IS NULL OR v_limit < 1 OR v_limit > 100 THEN
|
||||
RAISE_APPLICATION_ERROR(-20802, 'limit must be between 1 and 100');
|
||||
END IF;
|
||||
|
||||
-- Dynamic PL/SQL keeps ORDS gateway validation limited to the local handler
|
||||
-- package while CB_ORDS invokes only the fixed POC_2 API granted to it.
|
||||
EXECUTE IMMEDIATE
|
||||
'BEGIN poc_2.kb_select_ai_vpd_query_api.open_query(:prompt, :row_limit, :rows, :sql); END;'
|
||||
USING IN v_prompt, IN v_limit, OUT v_rows, OUT v_generated_sql;
|
||||
|
||||
:status_code := 200;
|
||||
OWA_UTIL.MIME_HEADER('application/json', FALSE);
|
||||
HTP.P('Cache-Control: no-store');
|
||||
OWA_UTIL.HTTP_HEADER_CLOSE;
|
||||
|
||||
APEX_JSON.OPEN_OBJECT;
|
||||
APEX_JSON.WRITE('profile', 'KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1');
|
||||
APEX_JSON.WRITE('generatedSql', v_generated_sql);
|
||||
APEX_JSON.WRITE('items', v_rows);
|
||||
APEX_JSON.CLOSE_OBJECT;
|
||||
|
||||
cb_ords_handler_pkg.clear_vpd_context;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
v_error_code := SQLCODE;
|
||||
v_error_message := SQLERRM;
|
||||
cb_ords_handler_pkg.clear_vpd_context;
|
||||
:status_code := CASE
|
||||
WHEN v_error_code IN (-20001, -20002) THEN 403
|
||||
WHEN v_error_code BETWEEN -20899 AND -20800 THEN 400
|
||||
WHEN v_error_code BETWEEN -20199 AND -20100 THEN 403
|
||||
ELSE 500
|
||||
END;
|
||||
SELECT JSON_OBJECT(
|
||||
'errorCode' VALUE v_error_code,
|
||||
'error' VALUE CASE
|
||||
WHEN v_error_code IN (-20001, -20002) THEN
|
||||
'VPD token is missing, invalid, expired, or has no access permission.'
|
||||
WHEN v_error_code BETWEEN -20899 AND -20800 THEN v_error_message
|
||||
ELSE 'Select AI query execution failed.'
|
||||
END
|
||||
RETURNING CLOB
|
||||
)
|
||||
INTO v_response
|
||||
FROM dual;
|
||||
OWA_UTIL.MIME_HEADER('application/json', FALSE);
|
||||
HTP.P('Cache-Control: no-store');
|
||||
OWA_UTIL.HTTP_HEADER_CLOSE;
|
||||
HTP.P(v_response);
|
||||
END;
|
||||
~',
|
||||
p_items_per_page => 0
|
||||
);
|
||||
|
||||
ORDS.DEFINE_PARAMETER(
|
||||
p_module_name => 'kb.select.ai.vpd',
|
||||
p_pattern => 'query',
|
||||
p_method => 'POST',
|
||||
p_name => 'Authorization',
|
||||
p_bind_variable_name => 'auth_header',
|
||||
p_source_type => 'HEADER',
|
||||
p_param_type => 'STRING',
|
||||
p_access_method => 'IN'
|
||||
);
|
||||
|
||||
ORDS.DEFINE_PARAMETER(
|
||||
p_module_name => 'kb.select.ai.vpd',
|
||||
p_pattern => 'query',
|
||||
p_method => 'POST',
|
||||
p_name => 'X-VPD-Probe-Id',
|
||||
p_bind_variable_name => 'probe_id',
|
||||
p_source_type => 'HEADER',
|
||||
p_param_type => 'STRING',
|
||||
p_access_method => 'IN'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === VPD-aware Select AI query endpoint ready ===
|
||||
PROMPT Path: /ords/cb-ords/kb-select-ai-vpd/query
|
||||
EXIT
|
||||
125
sql/adb/67_drop_legacy_kb_contracts_premium_vpd_cls_policy.sql
Normal file
@@ -0,0 +1,125 @@
|
||||
-- ============================================================
|
||||
-- 67_drop_legacy_kb_contracts_premium_vpd_cls_policy.sql
|
||||
--
|
||||
-- 목적:
|
||||
-- 과거 VPD Column-Level Security(CLS) PoC에서 생성된
|
||||
-- POC_2.KB_CONTRACTS.PREMIUM 대상 VPD 컬럼 정책을 제거한다.
|
||||
--
|
||||
-- 배경:
|
||||
-- 현재 운영 설계는 다음처럼 역할을 분리한다.
|
||||
--
|
||||
-- * VPD/DBMS_RLS : 행 수준 필터링만 담당
|
||||
-- * ASO/Data Redaction : 컬럼 마스킹만 담당
|
||||
--
|
||||
-- 따라서 DBMS_RLS.ADD_POLICY의 sec_relevant_cols 기반 컬럼 제어 정책은
|
||||
-- 운영 정책에서 제거한다. 이 스크립트는 named policy 하나만 삭제하며,
|
||||
-- ASO/Data Redaction 정책은 건드리지 않는다.
|
||||
--
|
||||
-- 삭제 대상:
|
||||
-- object_schema : POC_2
|
||||
-- object_name : KB_CONTRACTS
|
||||
-- policy_name : KB_PREMIUM_CLS_POLICY
|
||||
-- column : PREMIUM
|
||||
--
|
||||
-- 실행 권한:
|
||||
-- ADMIN 또는 DBMS_RLS.DROP_POLICY 수행 권한이 있는 계정.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET DEFINE OFF
|
||||
SET FEEDBACK ON
|
||||
|
||||
PROMPT === Before: legacy VPD CLS policy ===
|
||||
COLUMN object_owner FORMAT A18
|
||||
COLUMN object_name FORMAT A32
|
||||
COLUMN policy_name FORMAT A36
|
||||
COLUMN function FORMAT A40
|
||||
COLUMN enable FORMAT A10
|
||||
COLUMN policy_type FORMAT A20
|
||||
|
||||
SELECT object_owner,
|
||||
object_name,
|
||||
policy_name,
|
||||
pf_owner,
|
||||
package,
|
||||
function,
|
||||
enable,
|
||||
policy_type,
|
||||
sel
|
||||
FROM dba_policies
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name = 'KB_CONTRACTS'
|
||||
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
|
||||
|
||||
PROMPT === Before: security relevant columns ===
|
||||
COLUMN sec_rel_column FORMAT A32
|
||||
|
||||
SELECT object_owner,
|
||||
object_name,
|
||||
policy_name,
|
||||
sec_rel_column
|
||||
FROM dba_sec_relevant_cols
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name = 'KB_CONTRACTS'
|
||||
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
|
||||
|
||||
PROMPT === Dropping only the legacy VPD CLS policy ===
|
||||
DECLARE
|
||||
v_count NUMBER;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_count
|
||||
FROM dba_policies
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name = 'KB_CONTRACTS'
|
||||
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
|
||||
|
||||
IF v_count > 0 THEN
|
||||
DBMS_RLS.DROP_POLICY(
|
||||
object_schema => 'POC_2',
|
||||
object_name => 'KB_CONTRACTS',
|
||||
policy_name => 'KB_PREMIUM_CLS_POLICY'
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === After: legacy VPD CLS policy ===
|
||||
SELECT object_owner,
|
||||
object_name,
|
||||
policy_name,
|
||||
pf_owner,
|
||||
package,
|
||||
function,
|
||||
enable,
|
||||
policy_type,
|
||||
sel
|
||||
FROM dba_policies
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name = 'KB_CONTRACTS'
|
||||
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
|
||||
|
||||
PROMPT === After: security relevant columns ===
|
||||
SELECT object_owner,
|
||||
object_name,
|
||||
policy_name,
|
||||
sec_rel_column
|
||||
FROM dba_sec_relevant_cols
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name = 'KB_CONTRACTS'
|
||||
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
|
||||
|
||||
PROMPT === ASO/Data Redaction on PREMIUM is intentionally not changed ===
|
||||
COLUMN column_name FORMAT A24
|
||||
COLUMN function_type FORMAT A28
|
||||
|
||||
SELECT object_owner,
|
||||
object_name,
|
||||
column_name,
|
||||
function_type,
|
||||
function_parameters
|
||||
FROM redaction_columns
|
||||
WHERE object_owner = 'POC_2'
|
||||
AND object_name = 'KB_CONTRACTS'
|
||||
AND UPPER(column_name) = 'PREMIUM';
|
||||
|
||||
PROMPT === Legacy KB_CONTRACTS premium VPD CLS cleanup complete ===
|
||||
@@ -11,7 +11,41 @@ public record BackofficeProperties(
|
||||
Ai ai
|
||||
) {
|
||||
|
||||
public record Security(String adminUser, String adminPassword, boolean requireHttps) {
|
||||
public record Security(
|
||||
String adminUser,
|
||||
String adminPassword,
|
||||
String adminPasswordHash,
|
||||
boolean guestEnabled,
|
||||
String guestUser,
|
||||
String guestPassword,
|
||||
String guestPasswordHash,
|
||||
boolean requireHttps,
|
||||
boolean rememberMeEnabled,
|
||||
String rememberMeKey,
|
||||
int rememberMeDays
|
||||
) {
|
||||
|
||||
/** Remember-me is intentionally unavailable over HTTP or without a stable secret key. */
|
||||
public boolean rememberMeConfigured() {
|
||||
return requireHttps
|
||||
&& rememberMeEnabled
|
||||
&& rememberMeKey != null
|
||||
&& !rememberMeKey.isBlank()
|
||||
&& rememberMeDays >= 1
|
||||
&& rememberMeDays <= 90;
|
||||
}
|
||||
|
||||
public int rememberMeValiditySeconds() {
|
||||
return rememberMeDays * 24 * 60 * 60;
|
||||
}
|
||||
|
||||
public boolean guestConfigured() {
|
||||
return guestEnabled
|
||||
&& guestUser != null
|
||||
&& !guestUser.isBlank()
|
||||
&& ((guestPassword != null && !guestPassword.isBlank())
|
||||
|| (guestPasswordHash != null && !guestPasswordHash.isBlank()));
|
||||
}
|
||||
}
|
||||
|
||||
public record Token(int maxDays) {
|
||||
@@ -22,15 +56,20 @@ public record BackofficeProperties(
|
||||
|
||||
public record Ai(
|
||||
boolean enabled,
|
||||
String provider,
|
||||
String baseUrl,
|
||||
String model,
|
||||
String apiKey,
|
||||
Duration timeout,
|
||||
String embeddingModel
|
||||
String embeddingModel,
|
||||
String ociConfigFile,
|
||||
String ociProfile,
|
||||
String ociRegion,
|
||||
String ociCompartmentId
|
||||
) {
|
||||
|
||||
public Ai(boolean enabled, String baseUrl, String model, String apiKey, Duration timeout) {
|
||||
this(enabled, baseUrl, model, apiKey, timeout, "");
|
||||
this(enabled, "openai", baseUrl, model, apiKey, timeout, "", "", "", "", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* Authenticates only the streamable HTTP MCP endpoint with a service token.
|
||||
* Business-data authorization remains the bearerToken tool argument, which is
|
||||
* resolved to the VPD context by the ORDS handler.
|
||||
*/
|
||||
@Component
|
||||
public class McpAccessTokenFilter extends OncePerRequestFilter {
|
||||
|
||||
private final String accessToken;
|
||||
|
||||
public McpAccessTokenFilter(@Value("${backoffice.mcp.access-token:}") String accessToken) {
|
||||
this.accessToken = accessToken == null ? "" : accessToken.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
return !"/mcp".equals(request.getRequestURI());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
String bearerToken = bearerToken(request.getHeader("Authorization"));
|
||||
if (!accessToken.isBlank() && bearerToken != null && constantTimeEquals(accessToken, bearerToken)) {
|
||||
var authentication = new UsernamePasswordAuthenticationToken(
|
||||
"mcp-client",
|
||||
null,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_MCP"))
|
||||
);
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String bearerToken(String authorization) {
|
||||
if (authorization == null || !authorization.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||
return null;
|
||||
}
|
||||
String value = authorization.substring(7).trim();
|
||||
return value.isEmpty() ? null : value;
|
||||
}
|
||||
|
||||
private boolean constantTimeEquals(String expected, String actual) {
|
||||
return MessageDigest.isEqual(
|
||||
expected.getBytes(StandardCharsets.UTF_8),
|
||||
actual.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -20,7 +19,6 @@ public class OrdsClientConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("ordsAgentRestTemplate")
|
||||
RestTemplate ordsAgentRestTemplate(BackofficeProperties properties) {
|
||||
Duration timeout = properties.ords().agentTimeout();
|
||||
return new RestTemplateBuilder()
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
@@ -19,23 +21,48 @@ public class SecurityConfig {
|
||||
SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
BackofficeProperties properties,
|
||||
McpAccessTokenFilter mcpAccessTokenFilter
|
||||
UserDetailsService userDetailsService
|
||||
) throws Exception {
|
||||
if (properties.security().requireHttps()) {
|
||||
http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
|
||||
}
|
||||
|
||||
var security = properties.security();
|
||||
if (security.rememberMeConfigured()) {
|
||||
http.rememberMe(rememberMe -> rememberMe
|
||||
.key(security.rememberMeKey())
|
||||
.userDetailsService(userDetailsService)
|
||||
.rememberMeParameter("remember-me")
|
||||
.rememberMeCookieName("VPD_REMEMBER_ME")
|
||||
.tokenValiditySeconds(security.rememberMeValiditySeconds())
|
||||
.useSecureCookie(true)
|
||||
.alwaysRemember(false));
|
||||
}
|
||||
|
||||
return http
|
||||
.addFilterBefore(mcpAccessTokenFilter, BasicAuthenticationFilter.class)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(
|
||||
"/mcp", "/mcp/messages", "/mcp/*/messages", "/dds/mcp/messages"))
|
||||
"/mcp", "/mcp/messages", "/mcp/*/messages"))
|
||||
.headers(headers -> headers.httpStrictTransportSecurity(hsts -> hsts
|
||||
.includeSubDomains(true)
|
||||
.maxAgeInSeconds(31_536_000)))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/css/**", "/js/**", "/webjars/**", "/dds/mcp/sse", "/dds/mcp/messages")
|
||||
.requestMatchers(
|
||||
"/css/**", "/js/**", "/webjars/**",
|
||||
"/mcp", "/mcp/sse", "/mcp/*/sse", "/mcp/messages", "/mcp/*/messages")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/mcp").hasAnyRole("ADMIN", "MCP")
|
||||
.requestMatchers(HttpMethod.POST, "/login", "/logout").permitAll()
|
||||
.requestMatchers(HttpMethod.POST,
|
||||
"/probe",
|
||||
"/vector-knowledge/search",
|
||||
"/security-sql-scripts/explanation",
|
||||
"/mcp-chatbot",
|
||||
"/mcp-client-demo",
|
||||
"/mcp-reasoning")
|
||||
.authenticated()
|
||||
.requestMatchers(HttpMethod.PUT, "/**").hasRole("ADMIN")
|
||||
.requestMatchers(HttpMethod.PATCH, "/**").hasRole("ADMIN")
|
||||
.requestMatchers(HttpMethod.POST, "/**").hasRole("ADMIN")
|
||||
.requestMatchers(HttpMethod.DELETE, "/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated())
|
||||
.httpBasic(basic -> {
|
||||
})
|
||||
@@ -52,11 +79,26 @@ public class SecurityConfig {
|
||||
PasswordEncoder passwordEncoder
|
||||
) {
|
||||
var security = properties.security();
|
||||
var user = User.withUsername(security.adminUser())
|
||||
.password(passwordEncoder.encode(security.adminPassword()))
|
||||
String encodedPassword = security.adminPasswordHash() == null || security.adminPasswordHash().isBlank()
|
||||
? passwordEncoder.encode(security.adminPassword())
|
||||
: security.adminPasswordHash();
|
||||
List<UserDetails> users = new ArrayList<>();
|
||||
users.add(User.withUsername(security.adminUser())
|
||||
// BCrypt encoding includes a random salt. Re-encoding the configured
|
||||
// password on every startup would invalidate remember-me signatures.
|
||||
.password(encodedPassword)
|
||||
.roles("ADMIN")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
.build());
|
||||
if (security.guestConfigured()) {
|
||||
String encodedGuestPassword = security.guestPasswordHash() == null || security.guestPasswordHash().isBlank()
|
||||
? passwordEncoder.encode(security.guestPassword())
|
||||
: security.guestPasswordHash();
|
||||
users.add(User.withUsername(security.guestUser())
|
||||
.password(encodedGuestPassword)
|
||||
.roles("VIEWER")
|
||||
.build());
|
||||
}
|
||||
return new InMemoryUserDetailsManager(users);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record ColumnMaskingRule(
|
||||
long columnId,
|
||||
long objectId,
|
||||
String owner,
|
||||
String objectName,
|
||||
String columnName,
|
||||
long ruleId,
|
||||
String ruleCode,
|
||||
String ruleName,
|
||||
String templateCode,
|
||||
String ruleEnabledYn
|
||||
) {
|
||||
|
||||
public String targetLabel() {
|
||||
return owner + "." + objectName + "." + columnName;
|
||||
}
|
||||
|
||||
public MaskingTemplate template() {
|
||||
return MaskingTemplate.from(templateCode);
|
||||
}
|
||||
|
||||
public String ruleLabel() {
|
||||
return ruleName + " · " + template().label();
|
||||
}
|
||||
|
||||
public boolean ruleEnabled() {
|
||||
return "Y".equalsIgnoreCase(ruleEnabledYn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
/**
|
||||
* Read-only comparison between the configured masking metadata and the
|
||||
* corresponding Oracle Data Redaction policy stored in the database.
|
||||
*/
|
||||
public record MaskingPolicyStatus(
|
||||
String owner,
|
||||
String objectName,
|
||||
String policyName,
|
||||
String enabled,
|
||||
int configuredColumnCount,
|
||||
int appliedColumnCount,
|
||||
int mismatchedColumnCount,
|
||||
int legacyVpdColumnPolicyCount
|
||||
) {
|
||||
|
||||
public String targetLabel() {
|
||||
return owner + "." + objectName;
|
||||
}
|
||||
|
||||
public boolean policyEnabled() {
|
||||
return "YES".equalsIgnoreCase(enabled);
|
||||
}
|
||||
|
||||
/** A policy with no configured active column is correct only while disabled. */
|
||||
public boolean inactiveAsExpected() {
|
||||
return configuredColumnCount == 0 && !policyEnabled() && legacyVpdColumnPolicyCount == 0;
|
||||
}
|
||||
|
||||
public boolean applied() {
|
||||
return configuredColumnCount > 0
|
||||
&& policyEnabled()
|
||||
&& configuredColumnCount == appliedColumnCount
|
||||
&& mismatchedColumnCount == 0
|
||||
&& legacyVpdColumnPolicyCount == 0;
|
||||
}
|
||||
|
||||
public String statusLabel() {
|
||||
if (applied()) {
|
||||
return "적용됨";
|
||||
}
|
||||
if (inactiveAsExpected()) {
|
||||
return "미적용";
|
||||
}
|
||||
return "설정-DB 불일치";
|
||||
}
|
||||
|
||||
public String badgeClass() {
|
||||
if (applied()) {
|
||||
return "text-bg-success";
|
||||
}
|
||||
if (inactiveAsExpected()) {
|
||||
return "text-bg-secondary";
|
||||
}
|
||||
return "text-bg-danger";
|
||||
}
|
||||
|
||||
public String detail() {
|
||||
if (applied()) {
|
||||
return "활성 규칙과 DB 정책 컬럼이 일치합니다.";
|
||||
}
|
||||
if (legacyVpdColumnPolicyCount > 0) {
|
||||
return "기존 컬럼 NULL 정책(VPD CLS)이 활성입니다. ASO 설정과 별도로 값을 NULL 처리할 수 있습니다.";
|
||||
}
|
||||
if (inactiveAsExpected()) {
|
||||
return "활성 컬럼 기본 규칙이 없어 DB 정책을 중지 상태로 보관합니다.";
|
||||
}
|
||||
if (configuredColumnCount > 0 && !policyEnabled()) {
|
||||
return "활성 규칙이 있으나 DB 정책이 비활성입니다. 동기화가 필요합니다.";
|
||||
}
|
||||
if (configuredColumnCount == 0) {
|
||||
return "활성 규칙이 없는데 DB 정책이 활성입니다. 동기화가 필요합니다.";
|
||||
}
|
||||
return "설정 컬럼과 DB Redaction 컬럼이 다릅니다. 동기화가 필요합니다.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record MaskingRule(
|
||||
long ruleId,
|
||||
String ruleCode,
|
||||
String ruleName,
|
||||
String templateCode,
|
||||
String description,
|
||||
String enabledYn
|
||||
) {
|
||||
|
||||
public boolean enabled() {
|
||||
return "Y".equalsIgnoreCase(enabledYn);
|
||||
}
|
||||
|
||||
public MaskingTemplate template() {
|
||||
return MaskingTemplate.from(templateCode);
|
||||
}
|
||||
|
||||
public String templateLabel() {
|
||||
return template().label();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record MaskingRuleCreateCommand(
|
||||
String ruleCode,
|
||||
String ruleName,
|
||||
String templateCode,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Curated Data Redaction behaviours. A rule selects one template; operators
|
||||
* never enter raw DBMS_REDACT expressions from the backoffice UI.
|
||||
*/
|
||||
public enum MaskingTemplate {
|
||||
NULLIFY(
|
||||
"NULLIFY",
|
||||
"값 숨김 (NULL)",
|
||||
"값을 NULL로 반환합니다. ASO/Data Redaction 컬럼 마스킹에 사용합니다.",
|
||||
"DBMS_REDACT.NULLIFY",
|
||||
"NULL"),
|
||||
FULL(
|
||||
"FULL",
|
||||
"전체 마스킹",
|
||||
"전체 값을 가립니다. Oracle 기본값은 문자형 공백, 숫자형 0입니다.",
|
||||
"DBMS_REDACT.FULL",
|
||||
"문자형은 공백 / 숫자형은 0"),
|
||||
TEXT_PARTIAL(
|
||||
"TEXT_PARTIAL",
|
||||
"문자열 일부 마스킹",
|
||||
"첫 글자만 남기고 나머지를 가리는 사전 정의 문자열 규칙입니다.",
|
||||
"DBMS_REDACT.REGEXP",
|
||||
"A******** (예시)"),
|
||||
RRN_PARTIAL(
|
||||
"RRN_PARTIAL",
|
||||
"주민등록번호 부분 마스킹",
|
||||
"앞 6자리만 표시하고 나머지는 가리는 사전 정의 식별번호 규칙입니다.",
|
||||
"DBMS_REDACT.REGEXP",
|
||||
"900101-******* (예시)");
|
||||
|
||||
private final String code;
|
||||
private final String label;
|
||||
private final String description;
|
||||
private final String asoFunction;
|
||||
private final String previewResult;
|
||||
|
||||
MaskingTemplate(String code, String label, String description, String asoFunction, String previewResult) {
|
||||
this.code = code;
|
||||
this.label = label;
|
||||
this.description = description;
|
||||
this.asoFunction = asoFunction;
|
||||
this.previewResult = previewResult;
|
||||
}
|
||||
|
||||
public String code() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public String description() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String asoFunction() {
|
||||
return asoFunction;
|
||||
}
|
||||
|
||||
/** Human-readable result shown before an administrator assigns the rule. */
|
||||
public String previewResult() {
|
||||
return previewResult;
|
||||
}
|
||||
|
||||
public static MaskingTemplate from(String code) {
|
||||
return Arrays.stream(values())
|
||||
.filter(value -> value.code.equalsIgnoreCase(code))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 마스킹 템플릿입니다: " + code));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record UserMaskingRule(
|
||||
long userId,
|
||||
String username,
|
||||
long columnId,
|
||||
String owner,
|
||||
String objectName,
|
||||
String columnName,
|
||||
String ruleName,
|
||||
String templateCode,
|
||||
String decision,
|
||||
String activeYn
|
||||
) {
|
||||
|
||||
public boolean unmasked() {
|
||||
return "UNMASK".equalsIgnoreCase(decision);
|
||||
}
|
||||
|
||||
public boolean active() {
|
||||
return "Y".equalsIgnoreCase(activeYn);
|
||||
}
|
||||
|
||||
public String decisionLabel() {
|
||||
return unmasked() ? "원문 표시 허용" : "기본값과 동일 · 마스킹";
|
||||
}
|
||||
|
||||
public String targetLabel() {
|
||||
return owner + "." + objectName + "." + columnName;
|
||||
}
|
||||
|
||||
public String ruleLabel() {
|
||||
return ruleName + " · " + MaskingTemplate.from(templateCode).label();
|
||||
}
|
||||
}
|
||||
@@ -47,10 +47,10 @@ public record OperationStatusRow(
|
||||
return "ORDS path와 handler schema/module/template 매핑을 확인하세요.";
|
||||
}
|
||||
if (policyNames == null || policyNames.isBlank()) {
|
||||
return "VPD policy를 적용하세요.";
|
||||
return "행 접근 정책(VPD)을 적용하세요.";
|
||||
}
|
||||
if (policyEnabled == null || !policyEnabled.toUpperCase().contains("YES")) {
|
||||
return "VPD policy enable 상태를 확인하세요.";
|
||||
return "행 접근 정책(VPD) enable 상태를 확인하세요.";
|
||||
}
|
||||
if (functionStatus != null && !"VALID".equalsIgnoreCase(functionStatus)) {
|
||||
return "Policy function 컴파일 오류를 확인하세요.";
|
||||
|
||||
@@ -13,4 +13,69 @@ public record PermissionView(
|
||||
String filterPreview,
|
||||
String nullPolicyPreview
|
||||
) {
|
||||
|
||||
public String businessRuleSummary() {
|
||||
if (rules == null || rules.isBlank()) {
|
||||
return "-";
|
||||
}
|
||||
return java.util.Arrays.stream(rules.split(","))
|
||||
.map(String::trim)
|
||||
.filter(value -> !value.isBlank())
|
||||
.map(PermissionView::businessRuleLabel)
|
||||
.collect(java.util.stream.Collectors.joining(System.lineSeparator() + "AND "));
|
||||
}
|
||||
|
||||
public String storedRuleSummary() {
|
||||
return rules == null || rules.isBlank() ? "-" : rules;
|
||||
}
|
||||
|
||||
public String vpdMappingSummary() {
|
||||
return filterPreview == null || filterPreview.isBlank() ? "-" : filterPreview;
|
||||
}
|
||||
|
||||
public String columnControlSummary() {
|
||||
if (visibleColumns == null || visibleColumns.isBlank()) {
|
||||
return "컬럼 원문/마스킹은 컬럼 마스킹에서 관리";
|
||||
}
|
||||
return "기존 권한별 표시 예외 값: " + visibleColumns
|
||||
+ " · 신규 컬럼 제어는 컬럼 마스킹에서 관리";
|
||||
}
|
||||
|
||||
private static String businessRuleLabel(String rawRule) {
|
||||
String upper = rawRule.toUpperCase(java.util.Locale.ROOT);
|
||||
if ("ALL".equals(upper)) {
|
||||
return "전체 행";
|
||||
}
|
||||
if (upper.contains(" TOKEN_SUBJECT")) {
|
||||
return "토큰으로 식별된 이해관계자 본인 행";
|
||||
}
|
||||
if (upper.contains(" OWN_CONTRACT")) {
|
||||
return "담당 설계사 본인 계약";
|
||||
}
|
||||
if (upper.contains(" CHANNEL_CONTRACT")) {
|
||||
return "토큰 사용자의 채널 계약";
|
||||
}
|
||||
if (upper.contains(" OWN_CUSTOMER")) {
|
||||
return "담당 설계사 본인 계약에 연결된 고객/청구/외부보유";
|
||||
}
|
||||
if (upper.contains(" CHANNEL_CUSTOMER")) {
|
||||
return "토큰 사용자 채널 계약에 연결된 고객/청구/외부보유";
|
||||
}
|
||||
if (upper.contains(" STATIC_SQL ")) {
|
||||
return "정적 SQL 조건: " + rawRule.replaceFirst("(?i)^\\s*STATIC_SQL\\s+", "");
|
||||
}
|
||||
if (upper.startsWith("STATIC_SQL ")) {
|
||||
return "정적 SQL 조건: " + rawRule.substring("STATIC_SQL ".length());
|
||||
}
|
||||
if (upper.contains(" MY_DEPT")) {
|
||||
return "내 부서 행";
|
||||
}
|
||||
if (upper.contains(" SELF")) {
|
||||
return "내 사번/소유자 행";
|
||||
}
|
||||
if (upper.contains(" TAG ")) {
|
||||
return "태그 조건: " + rawRule;
|
||||
}
|
||||
return rawRule;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ public record ProbeResult(
|
||||
case INVALID_TOKEN -> "입력한 토큰 정보가 일치하지 않습니다.";
|
||||
case OBJECT_DISABLED -> "검증 대상이 비활성 상태입니다.";
|
||||
case OBJECT_NOT_ACCESSIBLE -> "현재 권한으로 이 대상에 접근할 수 없습니다.";
|
||||
case VPD_FILTER_ERROR -> "이 객체에 연결된 별도 VPD Filter가 실행되지 않습니다.";
|
||||
case VPD_FILTER_ERROR -> "이 객체에 연결된 행 접근 Filter가 실행되지 않습니다.";
|
||||
case ORDS_PATH_NOT_FOUND -> "검증 대상의 ORDS 경로를 찾지 못했습니다.";
|
||||
case ORDS_NOT_CONFIGURED -> "ORDS 연결 주소가 아직 설정되지 않았습니다.";
|
||||
case ORDS_UNAVAILABLE -> "ORDS 서버에 연결할 수 없습니다.";
|
||||
@@ -250,9 +250,9 @@ public record ProbeResult(
|
||||
|
||||
public String plainSummary() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "토큰의 사용자와 역할을 기준으로 VPD가 적용되었고, 허용된 데이터 " + rowCount
|
||||
case SUCCESS -> "토큰의 사용자와 역할을 기준으로 행 접근 정책이 적용되었고, 허용된 데이터 " + rowCount
|
||||
+ "개가 반환되었습니다.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "호출은 정상 처리됐지만 VPD가 현재 사용자에게 허용한 행은 0개입니다. 권한 규칙과 실제 데이터가 맞지 않으면 정상 결과이며 오류가 아닐 수 있습니다.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "호출은 정상 처리됐지만 행 접근 정책이 현재 사용자에게 허용한 행은 0개입니다. 행 접근 규칙과 실제 데이터가 맞지 않으면 정상 결과이며 오류가 아닐 수 있습니다.";
|
||||
case TOKEN_NOT_FOUND -> "입력한 원문과 일치하는 등록 기록이 현재 DB에 없습니다. 예전에 발급한 값이거나 다른 환경의 토큰일 수 있습니다.";
|
||||
case TOKEN_INACTIVE -> "토큰은 DB에 있지만 만료되었거나 관리자가 회수해 더 이상 사용자 권한을 증명할 수 없습니다.";
|
||||
case INVALID_TOKEN -> "화면에서 선택한 정보와 입력한 토큰 원문이 서로 다릅니다.";
|
||||
@@ -270,17 +270,17 @@ public record ProbeResult(
|
||||
|
||||
public String nextAction() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "반환된 행과 마스킹 컬럼이 예상한 범위인지 확인하세요. 다르면 권한 화면의 행·열 규칙을 조정한 뒤 다시 검증하세요.";
|
||||
case SUCCESS -> "반환된 행과 ASO 마스킹 컬럼이 예상한 범위인지 확인하세요. 행 범위가 다르면 행 접근 규칙을, 컬럼 표시가 다르면 컬럼 마스킹을 조정한 뒤 다시 검증하세요.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "유효 권한 화면에서 사용자에게 직접 또는 그룹으로 상속된 역할과 행 규칙을 확인하세요.";
|
||||
case TOKEN_NOT_FOUND -> "토큰 화면에서 현재 환경의 사용자에게 새 토큰을 발급하고, 한 번만 표시되는 원문을 복사해 다시 검증하세요.";
|
||||
case TOKEN_INACTIVE -> "토큰 화면에서 활성 토큰을 새로 발급한 뒤 다시 검증하세요.";
|
||||
case INVALID_TOKEN -> "복사한 원문이 맞는지 확인하고, 원문을 잃었다면 새 토큰을 발급하세요.";
|
||||
case OBJECT_DISABLED -> "보호 객체를 활성화하고 권한을 등록한 뒤 다시 검증하세요.";
|
||||
case OBJECT_NOT_ACCESSIBLE -> "유효 권한과 ORDS handler의 대상 객체가 같은지 확인하세요.";
|
||||
case VPD_FILTER_ERROR -> "토큰이나 권한을 바꾸지 말고 DB 보호 연결에서 이 객체의 Filter를 확인하세요. 일반 권한 객체라면 권한체계 자동 Filter로 복구하세요.";
|
||||
case VPD_FILTER_ERROR -> "토큰이나 행 접근 규칙을 바꾸지 말고 DB 보호 연결에서 이 객체의 Filter를 확인하세요. 일반 권한 객체라면 표준 행 접근 Filter로 복구하세요.";
|
||||
case ORDS_PATH_NOT_FOUND -> "보호 객체의 ORDS 경로와 실제 module/template 경로를 맞춘 뒤 다시 실행하세요.";
|
||||
case ORDS_NOT_CONFIGURED -> "설정에서 ORDS 기준 주소를 등록한 뒤 백오피스를 재시작하세요.";
|
||||
case ORDS_UNAVAILABLE, ORDS_TIMEOUT -> "권한 설정을 바꾸지 말고 먼저 ORDS 실행 상태, 주소와 네트워크를 확인하세요.";
|
||||
case ORDS_UNAVAILABLE, ORDS_TIMEOUT -> "행 접근 규칙을 바꾸지 말고 먼저 ORDS 실행 상태, 주소와 네트워크를 확인하세요.";
|
||||
case INVALID_ORDS_RESPONSE -> "ORDS handler가 rows 또는 items 배열을 반환하는지 확인하세요.";
|
||||
case UNKNOWN_ERROR -> "아래 기술 상세의 오류 코드와 응답을 확인한 뒤 해당 단계부터 점검하세요.";
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schemametadata;
|
||||
|
||||
public record SchemaAnnotation(String name, String value) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schemametadata;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SchemaMetadataColumn(
|
||||
String columnName,
|
||||
String dataType,
|
||||
boolean nullable,
|
||||
String comment,
|
||||
List<SchemaAnnotation> annotations
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schemametadata;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable;
|
||||
import java.util.List;
|
||||
|
||||
public record SchemaMetadataView(
|
||||
StructuredDataTable table,
|
||||
String tableComment,
|
||||
List<SchemaAnnotation> tableAnnotations,
|
||||
List<SchemaMetadataColumn> columns
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.securityscript;
|
||||
|
||||
/** Curated, version-controlled database script displayed read-only in the backoffice. */
|
||||
public record SecuritySqlScript(
|
||||
String scriptId,
|
||||
String category,
|
||||
String fileName,
|
||||
String title,
|
||||
String description,
|
||||
String source
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.securityscript;
|
||||
|
||||
/** Explanation generated from one immutable, curated security SQL script. */
|
||||
public record SecuritySqlScriptExplanation(
|
||||
String status,
|
||||
String modelName,
|
||||
String answer,
|
||||
String prompt,
|
||||
SecuritySqlScript script
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.securityscript;
|
||||
|
||||
/** Metadata for selecting a curated security SQL script without loading its source. */
|
||||
public record SecuritySqlScriptSummary(
|
||||
String scriptId,
|
||||
String category,
|
||||
String fileName,
|
||||
String title,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
/** A saved VPD policy or filter description, addressed by a stable composite key. */
|
||||
public record VpdDescriptionNote(String noteKey, String description) {
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.ColumnMaskingRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRuleCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.UserMaskingRule;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface MaskingRuleMapper {
|
||||
|
||||
List<MaskingRule> findAllRules();
|
||||
|
||||
List<MaskingRule> findEnabledRules();
|
||||
|
||||
MaskingRule findRuleById(@Param("ruleId") long ruleId);
|
||||
|
||||
MaskingRule findRuleByCode(@Param("ruleCode") String ruleCode);
|
||||
|
||||
long nextRuleId();
|
||||
|
||||
void insertRule(@Param("ruleId") long ruleId, @Param("command") MaskingRuleCreateCommand command);
|
||||
|
||||
int updateRuleActive(@Param("ruleId") long ruleId, @Param("enabledYn") String enabledYn);
|
||||
|
||||
List<ColumnMaskingRule> findColumnRules();
|
||||
|
||||
List<MaskingPolicyStatus> findPolicyStatuses();
|
||||
|
||||
ColumnMaskingRule findColumnRule(@Param("columnId") long columnId);
|
||||
|
||||
void upsertColumnRule(@Param("columnId") long columnId, @Param("ruleId") long ruleId);
|
||||
|
||||
int deleteColumnRule(@Param("columnId") long columnId);
|
||||
|
||||
int deleteUserRulesForColumn(@Param("columnId") long columnId);
|
||||
|
||||
List<UserMaskingRule> findUserRules();
|
||||
|
||||
UserMaskingRule findUserRule(@Param("userId") long userId, @Param("columnId") long columnId);
|
||||
|
||||
void upsertUserRule(
|
||||
@Param("userId") long userId,
|
||||
@Param("columnId") long columnId,
|
||||
@Param("decision") String decision
|
||||
);
|
||||
|
||||
int deleteUserRule(@Param("userId") long userId, @Param("columnId") long columnId);
|
||||
}
|
||||