Consolidate data access control backoffice updates

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

View File

@@ -1,31 +1,31 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('VPD 권한 운영')}"></head>
<head th:replace="~{fragments/layout :: head('데이터 접근 제어')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4 dashboard-page">
<header class="dashboard-hero">
<div>
<span class="architecture-kicker">VPD 권한 운영</span>
<h1>권한 운영 흐름</h1>
<span class="architecture-kicker">데이터 접근 제어</span>
<h1>행 접근과 컬럼 마스킹 운영 흐름</h1>
<p>업무 사용자와 데이터 접근 기준을 관리하고, DB가 적용한 결과까지 확인합니다.</p>
</div>
<div class="dashboard-hero-actions" aria-label="주요 작업">
<a class="btn rw-btn-primary" href="/probe">접근 검증</a>
<a class="btn rw-btn-secondary" href="/permissions">접근 규칙</a>
<a class="btn rw-btn-secondary" href="/permissions">접근 규칙</a>
</div>
<details class="explanation-details dashboard-explanation">
<summary>도움말: 메뉴와 권한 적용 구조 보기</summary>
<div class="product-help-overview">
<section class="product-help-section">
<h2>왜 권한 테이블을 따로 관리하나요?</h2>
<p>DB 연결은 VPD/DDS 실행 계정 하나로 유지하되, 실제 업무 사용자는 <code>CB_APP_USER</code>와 역할·권한 테이블에서 찾습니다. 요청마다 토큰이 사용자를 식별하고 VPD가 그 사용자의 권한만 조건으로 계산하므로, 같은 실행 계정으로도 사용자마다 다른 행과 컬럼을 안전하게 반환할 수 있습니다.</p>
<p>DB 연결은 공용 실행 계정 하나로 유지하되, 실제 업무 사용자는 <code>CB_APP_USER</code>와 역할·권한 테이블에서 찾습니다. 요청마다 토큰이 사용자를 식별하고 행 접근 정책이 그 사용자의 조건 계산하며, 컬럼 원문/마스킹은 ASO/Data Redaction 정책이 별도로 판단합니다.</p>
</section>
<section class="product-help-section">
<h2>메뉴 안내</h2>
<dl class="menu-role-grid">
<div><dt>권한 관리</dt><dd>사용자·그룹·역할과 접근 규칙을 정하고, 최종 권한을 확인합니다.</dd></div>
<div><dt>접근 제어 관리</dt><dd>사용자·그룹·역할과 접근 규칙, 컬럼 마스킹 설정을 정하고 최종 적용 결과를 확인합니다.</dd></div>
<div><dt>보호·검증</dt><dd>보호 정책을 연결하고 토큰으로 실제 DB 조회 결과를 검증합니다.</dd></div>
<div><dt>연동 도구</dt><dd>조회 대상, ORDS 연동, 지식 검색과 MCP 연결을 관리합니다.</dd></div>
<div><dt>운영</dt><dd>현재 연결과 실행 상태를 확인합니다.</dd></div>
@@ -38,8 +38,8 @@
<p>권한을 먼저 만들고 보호 대상을 연결한 뒤, 실제 사용자 토큰으로 결과를 검증합니다. 권한은 토큰에 복사되지 않아 이후 변경도 다음 요청부터 반영됩니다.</p>
<div class="diagram-canvas">
<svg class="product-flow-diagram setup-flow-diagram" viewBox="0 0 1120 245" role="img" aria-labelledby="setup-flow-title setup-flow-desc">
<title id="setup-flow-title">VPD 권한 설정과 검증 순서</title>
<desc id="setup-flow-desc">사용자와 그룹, 역할, 접근 규칙, 보호 대상을 차례로 설정하고 유효 권한과 실제 조회 결과를 검증하는 흐름</desc>
<title id="setup-flow-title">데이터 접근 제어 설정과 검증 순서</title>
<desc id="setup-flow-desc">사용자와 그룹, 역할, 접근 규칙, 컬럼 마스킹, 보호 대상을 차례로 설정하고 실제 조회 결과를 검증하는 흐름</desc>
<defs><marker id="setup-flow-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>
<path class="diagram-link" marker-end="url(#setup-flow-arrow)" d="M205 100 H238"/>
<path class="diagram-link" marker-end="url(#setup-flow-arrow)" d="M415 100 H448"/>
@@ -47,11 +47,11 @@
<path class="diagram-link" marker-end="url(#setup-flow-arrow)" d="M835 100 H868"/>
<g class="diagram-node diagram-node-step"><rect x="30" y="52" width="175" height="96" rx="12"/><circle cx="57" cy="77" r="15"/><text x="57" y="82" class="diagram-step-number">1</text><text x="117" y="85" class="diagram-node-title">사용자·그룹</text><text x="117" y="113" class="diagram-node-detail">업무 대상을 등록</text></g>
<g class="diagram-node diagram-node-step"><rect x="240" y="52" width="175" height="96" rx="12"/><circle cx="267" cy="77" r="15"/><text x="267" y="82" class="diagram-step-number">2</text><text x="327" y="85" class="diagram-node-title">역할</text><text x="327" y="113" class="diagram-node-detail">직접·그룹 역할 부여</text></g>
<g class="diagram-node diagram-node-step diagram-node-accent"><rect x="450" y="52" width="175" height="96" rx="12"/><circle cx="477" cy="77" r="15"/><text x="477" y="82" class="diagram-step-number">3</text><text x="537" y="85" class="diagram-node-title">접근 규칙</text><text x="537" y="113" class="diagram-node-detail">객체·행·컬럼 설정</text></g>
<g class="diagram-node diagram-node-step diagram-node-accent"><rect x="660" y="52" width="175" height="96" rx="12"/><circle cx="687" cy="77" r="15"/><text x="687" y="82" class="diagram-step-number">4</text><text x="747" y="85" class="diagram-node-title">보호·연결</text><text x="747" y="113" class="diagram-node-detail">VPD·ORDS 대상 확인</text></g>
<g class="diagram-node diagram-node-step diagram-node-accent"><rect x="450" y="52" width="175" height="96" rx="12"/><circle cx="477" cy="77" r="15"/><text x="477" y="82" class="diagram-step-number">3</text><text x="537" y="85" class="diagram-node-title">접근 규칙</text><text x="537" y="113" class="diagram-node-detail">객체·행 조건 설정</text></g>
<g class="diagram-node diagram-node-step diagram-node-accent"><rect x="660" y="52" width="175" height="96" rx="12"/><circle cx="687" cy="77" r="15"/><text x="687" y="82" class="diagram-step-number">4</text><text x="747" y="85" class="diagram-node-title">보호·연결</text><text x="747" y="113" class="diagram-node-detail">정책·ORDS 대상 확인</text></g>
<g class="diagram-node diagram-node-step diagram-node-data"><rect x="870" y="52" width="220" height="96" rx="12"/><circle cx="897" cy="77" r="15"/><text x="897" y="82" class="diagram-step-number">5</text><text x="977" y="85" class="diagram-node-title">유효 권한·접근 검증</text><text x="977" y="113" class="diagram-node-detail">토큰으로 실제 결과 확인</text></g>
<path class="setup-flow-return" d="M980 166 V192 H117 V166"/>
<text x="548" y="218" class="setup-flow-note">규칙 변경은 다음 요청의 VPD 조건 계산에 동적으로 반영됩니다.</text>
<text x="548" y="218" class="setup-flow-note">규칙 변경은 다음 요청의 조건 계산에 동적으로 반영됩니다.</text>
</svg>
</div>
</section>
@@ -60,8 +60,8 @@
<h2>요청마다 권한이 적용되는 흐름</h2>
<div class="diagram-canvas">
<svg class="product-flow-diagram" viewBox="0 0 1120 270" role="img" aria-labelledby="vpd-flow-title vpd-flow-desc">
<title id="vpd-flow-title">VPD 권한 적용 흐름</title>
<desc id="vpd-flow-desc">Bearer Token 요청이 공용 실행 계정과 사용자 컨텍스트를 거쳐 권한 테이블에서 계산한 VPD 조건으로 보호 데이터를 조회하는 흐름</desc>
<title id="vpd-flow-title">행 접근 적용 흐름</title>
<desc id="vpd-flow-desc">Bearer Token 요청이 공용 실행 계정과 사용자 컨텍스트를 거쳐 권한 테이블에서 계산한 조건으로 보호 데이터를 조회하는 흐름</desc>
<defs><marker id="flow-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M190 92 H245"/>
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M395 92 H450"/>
@@ -71,8 +71,8 @@
<g class="diagram-node"><rect x="30" y="48" width="160" height="88" rx="12"/><text x="110" y="81" class="diagram-node-title">요청·토큰</text><text x="110" y="108" class="diagram-node-detail">Bearer Token</text></g>
<g class="diagram-node"><rect x="245" y="48" width="150" height="88" rx="12"/><text x="320" y="78" class="diagram-node-title">공용 실행 계정</text><text x="320" y="105" class="diagram-node-detail">CB_ORDS</text><text x="320" y="123" class="diagram-node-note">한 개의 DB 연결</text></g>
<g class="diagram-node"><rect x="450" y="48" width="150" height="88" rx="12"/><text x="525" y="78" class="diagram-node-title">사용자 컨텍스트</text><text x="525" y="105" class="diagram-node-detail">CB_AGENT_CTX</text><text x="525" y="123" class="diagram-node-note">사용자별로 설정</text></g>
<g class="diagram-node diagram-node-accent"><rect x="655" y="48" width="150" height="88" rx="12"/><text x="730" y="78" class="diagram-node-title">VPD 조건 계산</text><text x="730" y="105" class="diagram-node-detail">ALLOW · DENY · 행 · 열</text></g>
<g class="diagram-node diagram-node-accent"><rect x="860" y="48" width="210" height="88" rx="12"/><text x="965" y="78" class="diagram-node-title">보호 데이터 조회</text><text x="965" y="105" class="diagram-node-detail">허용된 행·컬럼만 반환</text></g>
<g class="diagram-node diagram-node-accent"><rect x="655" y="48" width="150" height="88" rx="12"/><text x="730" y="78" class="diagram-node-title"> 조건 계산</text><text x="730" y="105" class="diagram-node-detail">ALLOW · DENY · predicate</text></g>
<g class="diagram-node diagram-node-accent"><rect x="860" y="48" width="210" height="88" rx="12"/><text x="965" y="78" class="diagram-node-title">보호 데이터 조회</text><text x="965" y="105" class="diagram-node-detail">허용된 행 반환</text></g>
<g class="diagram-node diagram-node-data"><rect x="570" y="180" width="300" height="62" rx="12"/><text x="720" y="207" class="diagram-node-title">사용자 · 역할 · 권한 테이블</text><text x="720" y="229" class="diagram-node-detail">요청 시 동적으로 조회</text></g>
</svg>
</div>
@@ -82,7 +82,7 @@
<h2>권한 데이터 모델</h2>
<div class="diagram-canvas">
<svg class="permission-erd-diagram" viewBox="0 0 1180 355" role="img" aria-labelledby="erd-title erd-desc">
<title id="erd-title">VPD 권한 ERD</title>
<title id="erd-title">접근 제어 ERD</title>
<desc id="erd-desc">사용자와 그룹에서 역할을 얻고 역할에서 권한과 권한 규칙을 거쳐 보호 대상으로 연결되는 데이터 모델</desc>
<defs><marker id="erd-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>
<path class="diagram-link" marker-end="url(#erd-arrow)" d="M175 90 H230"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M390 90 H445"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M605 90 H660"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M820 90 H875"/>
@@ -92,7 +92,7 @@
<g class="erd-entity"><rect x="230" y="50" width="160" height="80" rx="10"/><text x="310" y="80">CB_USER_ROLE</text><text x="310" y="105">직접 역할 연결</text></g>
<g class="erd-entity"><rect x="445" y="50" width="160" height="80" rx="10"/><text x="525" y="80">CB_APP_ROLE</text><text x="525" y="105">역할</text></g>
<g class="erd-entity"><rect x="660" y="50" width="160" height="80" rx="10"/><text x="740" y="80">CB_PERMISSION</text><text x="740" y="105">객체 접근</text></g>
<g class="erd-entity"><rect x="875" y="50" width="160" height="80" rx="10"/><text x="955" y="80">CB_PERMISSION_RULE</text><text x="955" y="105">·열 조건</text></g>
<g class="erd-entity"><rect x="875" y="50" width="160" height="80" rx="10"/><text x="955" y="80">CB_PERMISSION_RULE</text><text x="955" y="105">행 조건 매핑</text></g>
<g class="erd-entity erd-entity-accent"><rect x="1045" y="50" width="115" height="80" rx="10"/><text x="1102" y="80">보호 대상</text><text x="1102" y="105">TABLE / VIEW</text></g>
<g class="erd-entity"><rect x="25" y="205" width="150" height="80" rx="10"/><text x="100" y="235">CB_USER_GROUP</text><text x="100" y="260">그룹 소속</text></g>
<g class="erd-entity"><rect x="230" y="205" width="160" height="80" rx="10"/><text x="310" y="235">CB_GROUP</text><text x="310" y="260">사용자 그룹</text></g>
@@ -119,10 +119,10 @@
<section class="dashboard-menu-flow" aria-labelledby="menu-flow-title">
<div class="dashboard-section-heading">
<span class="architecture-kicker">업무 흐름</span>
<h2 id="menu-flow-title">설정은 권한 관리에서, 적용 확인은 접근 검증에서</h2>
<h2 id="menu-flow-title">설정은 접근 제어에서, 적용 확인은 접근 검증에서</h2>
</div>
<div class="dashboard-flow-grid">
<a class="dashboard-flow-card" href="/permissions"><strong>권한 관리</strong><span>사용자·역할·접근 규칙</span></a>
<a class="dashboard-flow-card" href="/permissions"><strong>행 접근 규칙</strong><span>역할별 객체·행 조건</span></a>
<a class="dashboard-flow-card" href="/vpd-policies"><strong>보호·검증</strong><span>보호 연결·검증 세션·접근 확인</span></a>
<a class="dashboard-flow-card" href="/objects"><strong>연동 도구</strong><span>조회 대상·지식 검색·MCP 연동</span></a>
<a class="dashboard-flow-card" href="/operation-status"><strong>운영</strong><span>상태와 실행 결과 확인</span></a>
@@ -134,7 +134,7 @@
<h2>바로 시작</h2>
<div class="dashboard-action-grid">
<a class="dashboard-action" href="/permissions">
<span><strong>접근 규칙</strong><small>역할별 객체·행·컬럼 접근을 설정합니다.</small></span>
<span><strong>접근 규칙</strong><small>역할별 객체·행 조건을 설정합니다. 컬럼은 ASO 마스킹에서 관리합니다.</small></span>
<span class="dashboard-action-arrow" aria-hidden="true"></span>
</a>
<a class="dashboard-action dashboard-action-emphasis" href="/probe">
@@ -161,7 +161,7 @@
<div class="section-heading">
<div>
<h2>현재 검증 가능한 데이터</h2>
<p class="section-subtitle">권한 규칙과 ORDS 경로가 등록된 보호 객체입니다.</p>
<p class="section-subtitle">행 접근 규칙과 ORDS 경로가 등록된 보호 객체입니다.</p>
</div>
<a class="btn btn-sm rw-btn-primary" href="/probe">접근 검증</a>
</div>
@@ -174,7 +174,7 @@
<td><code th:text="${object.ordsPath()}">path</code></td>
<td><span class="badge text-bg-success">사용 가능</span></td>
</tr>
<tr th:if="${#lists.isEmpty(objects)}"><td colspan="3" class="text-muted">아직 검증할 보호 객체가 없습니다. 권한 규칙에서 객체 권한을 먼저 등록하세요.</td></tr>
<tr th:if="${#lists.isEmpty(objects)}"><td colspan="3" class="text-muted">아직 검증할 보호 객체가 없습니다. 행 접근 규칙에서 객체 접근을 먼저 등록하세요.</td></tr>
</tbody>
</table>
</div>

View File

@@ -8,7 +8,7 @@
<h1>사용자별 접근 확인</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>직접 역할과 그룹 상속 역할을 합쳐 최종 권한을 계산합니다. 여기의 결과는 토큰을 발급해 ORDS/VPD 조회를 검증하기 전 확인하는 설계 근거입니다.</p>
<p>직접 역할과 그룹 상속 역할을 합쳐 최종 권한을 계산합니다. 여기의 결과는 토큰을 발급해 ORDS 행 접근 조회를 검증하기 전 확인하는 설계 근거입니다.</p>
</details>
</div>

View File

@@ -3,7 +3,9 @@
<head th:fragment="head(title)">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${title == 'VPD 권한 운영' ? title : title + ' · VPD 권한 운영'}">VPD 권한 운영</title>
<meta name="backoffice-can-mutate" th:content="${canMutate}">
<meta name="backoffice-read-only" th:content="${readOnlyMode}">
<title th:text="${title == '데이터 접근 제어' ? title : title + ' · 데이터 접근 제어'}">데이터 접근 제어</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/app.css" rel="stylesheet">
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
@@ -13,15 +15,15 @@
<body>
<nav th:fragment="nav" class="navbar rw-nav navbar-expand-lg" data-product-nav>
<div class="container rw-nav-top">
<a class="navbar-brand" href="/">VPD 권한 운영</a>
<a class="navbar-brand" href="/">데이터 접근 제어</a>
<div class="rw-menu" aria-label="주요 메뉴">
<button class="rw-menu-trigger" type="button" data-submenu-trigger="access" aria-controls="submenu-access" aria-expanded="false">권한 관리</button>
<button class="rw-menu-trigger" type="button" data-submenu-trigger="access" aria-controls="submenu-access" aria-expanded="false">접근 제어</button>
<button class="rw-menu-trigger" type="button" data-submenu-trigger="protection" aria-controls="submenu-protection" aria-expanded="false">보호·검증</button>
<button class="rw-menu-trigger" type="button" data-submenu-trigger="integration" aria-controls="submenu-integration" aria-expanded="false">연동 도구</button>
<button class="rw-menu-trigger" type="button" data-submenu-trigger="operations" aria-controls="submenu-operations" aria-expanded="false">운영</button>
</div>
<div class="rw-nav-utility ms-auto">
<a class="rw-quick-link" href="/permissions">권한 설정</a>
<a class="rw-quick-link" href="/permissions">행 접근 규칙</a>
<a class="rw-quick-link" href="/tokens">토큰 발급</a>
<button class="rw-admin-trigger" type="button" data-submenu-trigger="admin" aria-controls="submenu-admin" aria-expanded="false">관리자</button>
<form method="post" action="/logout">
@@ -32,15 +34,17 @@
</div>
<div class="rw-submenu-bar" data-submenu-bar hidden>
<div class="container">
<div id="submenu-access" class="rw-submenu" data-submenu-panel="access" role="navigation" aria-label="권한 관리 메뉴" hidden>
<div id="submenu-access" class="rw-submenu" data-submenu-panel="access" role="navigation" aria-label="접근 제어 메뉴" hidden>
<a class="nav-link" href="/users">사용자 관리</a>
<a class="nav-link" href="/groups">그룹 관리</a>
<a class="nav-link" href="/roles">역할 관리</a>
<a class="nav-link" href="/permissions">권한 설정</a>
<a class="nav-link" href="/permissions">행 접근 규칙</a>
<a class="nav-link" href="/user-masking-rules">컬럼 원문 표시 허용</a>
<a class="nav-link" href="/effective-matrix">권한 현황</a>
</div>
<div id="submenu-protection" class="rw-submenu" data-submenu-panel="protection" role="navigation" aria-label="보호 및 검증 메뉴" hidden>
<a class="nav-link" href="/vpd-policies">보호 상태</a>
<a class="nav-link" href="/masking-rules">컬럼 마스킹</a>
<a class="nav-link" href="/tokens">토큰 발급</a>
<a class="nav-link" href="/probe">접근 검증</a>
</div>
@@ -58,6 +62,9 @@
<a class="nav-link" href="/operation-status">운영 현황</a>
</div>
<div id="submenu-admin" class="rw-submenu" data-submenu-panel="admin" role="navigation" aria-label="관리자 메뉴" hidden>
<a class="nav-link" href="/vpd-filter-runtime">행 접근 필터 구조</a>
<a class="nav-link" href="/schema-metadata">DB 메타데이터</a>
<a class="nav-link" href="/security-sql-scripts">보안 SQL 스크립트</a>
<a class="nav-link" href="/vpd-filter-policies">고급 접근 조건</a>
<a class="nav-link" href="/settings">시스템 설정</a>
<a class="nav-link" href="/settings/database">DB 준비 상태</a>

View File

@@ -48,24 +48,41 @@
<section class="result-section">
<div class="section-heading compact-heading">
<h3>VPD가 적용된 실제 결과</h3>
<h3>행 접근 정책이 적용된 실제 결과</h3>
<span th:if="${selectedObject}"><code th:text="${selectedObject.displayName()}">ADMIN.OBJECT</code></span>
</div>
<div class="result-metrics">
<div><span>보이는</span><strong th:text="${result.rowCount()}">0</strong></div>
<div><span>행 접근 후 남은</span><strong th:text="${result.rowCount()}">0</strong></div>
<div>
<span>NULL/마스킹으로 확인 컬럼</span>
<span>ASO 마스킹 확인 컬럼</span>
<strong th:if="${!#lists.isEmpty(result.maskedColumns())}" th:text="${#strings.listJoin(result.maskedColumns(), ', ')}">column</strong>
<strong th:if="${#lists.isEmpty(result.maskedColumns())}">없음</strong>
</div>
</div>
<div class="effective-preview mt-3">
<dl>
<div>
<dt>행 접근 판단</dt>
<dd th:text="${result.rowCount() == 0 ? '토큰 context와 접근 규칙 기준으로 반환 가능한 행이 없습니다.' : '토큰 context와 접근 규칙 기준으로 반환 가능한 행이 있습니다.'}">반환 가능</dd>
</div>
<div>
<dt>ASO 컬럼 표시 판단</dt>
<dd th:if="${!#lists.isEmpty(result.maskedColumns())}" th:text="${'마스킹 또는 NULL로 반환된 민감 컬럼: ' + #strings.listJoin(result.maskedColumns(), ', ')}">마스킹 컬럼</dd>
<dd th:if="${#lists.isEmpty(result.maskedColumns())}">응답 기준으로 전체 NULL/마스킹 처리된 민감 컬럼을 찾지 못했습니다. 원문 허용이거나 해당 컬럼이 응답에 없을 수 있습니다.</dd>
</div>
<div>
<dt>확인 기준</dt>
<dd>행 개수는 행 접근 정책 결과이고, 컬럼 값은 ASO/Data Redaction 결과입니다. 두 판단은 별도로 봐야 합니다.</dd>
</div>
</dl>
</div>
<div class="vector-result-panel mt-3"
th:if="${vectorSearch and !#lists.isEmpty(result.rows())}">
<div class="section-heading compact-heading">
<div>
<h3>벡터 검색 Top-K</h3>
<p class="section-subtitle">거리(SCORE)가 낮은 순서로 VPD를 통과한 검색 단위를 반환했습니다.</p>
<p class="section-subtitle">거리(SCORE)가 낮은 순서로 행 접근 정책을 통과한 검색 단위를 반환했습니다.</p>
</div>
<span class="badge text-bg-light" th:text="${'K=' + result.rowCount()}">K=0</span>
</div>
@@ -145,8 +162,8 @@
<div>
<h3>참고용 SQL 재현</h3>
<p class="section-subtitle"
th:text="${vectorSearch ? 'VECTOR_DISTANCE 검색과 권한 규칙을 조합한 참고용 표현입니다.' : '현재 권한 규칙을 조합한 참고용 표현입니다.'}">
현재 권한 규칙을 조합한 참고용 표현입니다.
th:text="${vectorSearch ? 'VECTOR_DISTANCE 검색과 행 접근 규칙을 조합한 참고용 표현입니다.' : '현재 행 접근 규칙을 조합한 참고용 표현입니다.'}">
현재 행 접근 규칙을 조합한 참고용 표현입니다.
</p>
</div>
<span class="badge text-bg-light">참고용</span>
@@ -154,7 +171,7 @@
<div class="probe-exchange-grid mt-3">
<section class="probe-exchange" data-sql-trace-field="vpd_context" th:if="${tokenContext}">
<h3>set_vpd_context 사용자 컨텍스트</h3>
<p class="form-hint">이 컨텍스트로 권한 규칙을 계산한 뒤 아래 VPD predicate와 effective SQL을 만들었습니다.</p>
<p class="form-hint">이 컨텍스트로 행 접근 규칙을 계산한 뒤 아래 VPD predicate와 effective SQL을 만들었습니다.</p>
<dl class="mb-0">
<div><dt>CB_AGENT_CTX.USER_ID</dt><dd th:text="${tokenContext.userId()}">103</dd></div>
<div><dt>사용자</dt><dd th:text="${tokenContext.username()}">김어드민</dd></div>
@@ -180,7 +197,7 @@ ROWNUM &lt;= :row_limit</pre>
<p class="form-hint mb-0">이 부분이 검색어 벡터와 저장 벡터의 거리 계산입니다. 아래에는 실제로 등록된 역할 기반 행 접근 조건만 표시됩니다.</p>
</section>
<section class="probe-exchange" data-sql-trace-field="vpd_predicate">
<h3 th:text="${vectorSearch ? '역할 기반 권한 필터 (재현)' : 'VPD 조건 (재현)'}">VPD 조건 (재현)</h3>
<h3 th:text="${vectorSearch ? '역할 기반 권한 필터 (재현)' : '행 접근 조건 (VPD predicate 재현)'}">행 접근 조건 (VPD predicate 재현)</h3>
<pre th:if="${result.vpdPredicate() == '1 = 1'}">1 = 1 (ALL: 추가 행 필터 없음)</pre>
<pre th:unless="${result.vpdPredicate() == '1 = 1'}" th:text="${result.vpdPredicate()}">(DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))</pre>
<p class="form-hint mb-0" th:if="${vectorSearch}">선택한 사용자의 직접 역할·그룹 상속 역할에 연결된 permission rule에서 계산됩니다. 기본 whitelist 역할의 ALL은 추가 행 필터 없이 조회를 허용하고, 실제로 TAG·부서 조건을 등록한 역할만 그 조건이 SQL에 들어갑니다. 권한이 없으면 <code>1 = 0</code>입니다.</p>
@@ -191,7 +208,7 @@ ROWNUM &lt;= :row_limit</pre>
</section>
</div>
<p class="form-hint mt-2 mb-0">
이 영역은 권한 규칙을 읽기 쉽게 재현한 표현일 뿐, 실행 증적이 아닙니다. 실제 SQL과 VPD predicate는 위의 DB 감사 실행 증적(FGA)으로 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
이 영역은 행 접근 규칙을 읽기 쉽게 재현한 표현일 뿐, 실행 증적이 아닙니다. 실제 SQL과 VPD predicate는 위의 DB 감사 실행 증적(FGA)으로 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
</p>
</section>
@@ -209,7 +226,7 @@ ROWNUM &lt;= :row_limit</pre>
<p th:text="${result.nextAction()}">다음 행동</p>
<div class="action-stack">
<a class="btn btn-sm rw-btn-secondary" href="/effective-matrix">사용자별 최종 권한 보기</a>
<a class="btn btn-sm rw-btn-secondary" href="/permissions">권한 규칙 보기</a>
<a class="btn btn-sm rw-btn-secondary" href="/permissions">행 접근 규칙 보기</a>
<a class="btn btn-sm rw-btn-secondary" href="/tokens" th:if="${result.status().name() == 'TOKEN_NOT_FOUND' || result.status().name() == 'TOKEN_INACTIVE'}">새 토큰 발급</a>
</div>
</section>

View File

@@ -0,0 +1,25 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="explanation">
<section class="alert alert-warning mb-3" th:if="${errorMessage}" th:text="${errorMessage}">설명을 생성할 수 없습니다.</section>
<section th:if="${explanation}" class="ai-answer">
<div class="section-heading mb-2">
<div>
<h2>LLM 스크립트 설명</h2>
<p class="section-subtitle" th:text="${explanation.script().fileName() + ' · 토큰 처리와 행 접근/ASO 적용 흐름 포함'}">script.sql</p>
</div>
<div class="action-stack">
<span class="badge text-bg-secondary" th:text="${explanation.modelName()} ?: 'model not configured'">model</span>
<span class="badge" th:classappend="${explanation.status() == 'SUCCESS'} ? ' text-bg-success' : ' text-bg-warning'" th:text="${explanation.status()}">SUCCESS</span>
</div>
</div>
<div class="markdown-view" data-markdown-view th:text="${explanation.answer()}">설명</div>
<details class="explanation-details mt-3">
<summary>LLM에 전달한 근거 Prompt 보기</summary>
<pre th:text="${explanation.prompt()}">prompt</pre>
</details>
</section>
</div>
</body>
</html>

View File

@@ -1,13 +1,13 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('VPD 권한 운영')}"></head>
<head th:replace="~{fragments/layout :: head('데이터 접근 제어')}"></head>
<body class="login-body">
<main class="login-shell">
<section class="login-panel">
<div class="login-brand">
<span class="login-mark">VPD</span>
<span class="login-mark">접근</span>
<div>
<h1>VPD 권한 운영 콘솔</h1>
<h1>데이터 접근 제어 콘솔</h1>
</div>
</div>
@@ -28,6 +28,13 @@
Admin Password
<input class="form-control" name="password" type="password" autocomplete="current-password" required>
</label>
<label class="form-check" th:if="${rememberMeAvailable}">
<!-- Persistent login is opt-out for this internal HTTPS backoffice.
The server issues only a signed HttpOnly/Secure cookie; no password
or bearer token is stored in browser Web Storage. -->
<input class="form-check-input" id="remember-me" name="remember-me" type="checkbox" value="true" checked>
<span class="form-check-label" for="remember-me">로그인 유지 <small class="text-muted" th:text="${'이 기기에서 ' + rememberMeDays + '일간 유지'}">이 기기에서 14일간 유지</small></span>
</label>
<button class="btn rw-btn-primary w-100" type="submit">로그인</button>
</form>
</section>

View File

@@ -0,0 +1,262 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('컬럼 마스킹')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<div class="page-title">
<h1>컬럼 마스킹</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>마스킹 방법을 사전 정의 템플릿으로 등록하고, 민감 컬럼마다 하나의 기본 규칙을 연결합니다. 컬럼 연결 목록은 <strong>마스킹 대상 블랙리스트</strong>이며, 연결하지 않은 컬럼은 ASO 마스킹 대상이 아닙니다. 행 접근은 행 접근 정책이 계속 담당하며 이 화면은 반환값 표시 방식만 다룹니다.</p>
<p>연결된 컬럼은 행 접근 정책이 조회를 허용한 사용자에게도 기본적으로 마스킹됩니다. <a href="/user-masking-rules">컬럼 원문 표시 허용 사용자</a>에 등록한 사용자만 원문을 봅니다. 이 설정은 행 접근 권한을 추가하지 않습니다. 컬럼 연결·해제 또는 연결된 규칙의 활성 상태 변경 시 DBMS_REDACT 정책도 자동 동기화됩니다.</p>
<section class="content-band mt-3 mb-0">
<h2>ASO 마스킹 적용 흐름</h2>
<p><strong>ASO/Data Redaction은 권한 테이블을 직접 조회하지 않습니다.</strong> 토큰 처리 중 <code>set_masking_rule_context(user_id)</code>가 사용자별 마스킹 예외를 세션 context로 만들고, ASO 정책은 그 context만 읽습니다.</p>
<div class="diagram-canvas">
<svg class="product-flow-diagram" viewBox="0 0 1120 410" role="img" aria-labelledby="aso-flow-title aso-flow-desc">
<title id="aso-flow-title">Bearer 토큰 기반 ASO 마스킹 적용 흐름</title>
<desc id="aso-flow-desc">Bearer 토큰으로 사용자 컨텍스트와 컬럼별 MR context를 만들고, Oracle Data Redaction이 MR context 값에 따라 원문 또는 마스킹값을 반환한다.</desc>
<defs>
<marker id="aso-flow-arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="currentColor"/></marker>
</defs>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M185 92 H225"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M385 92 H425"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M585 92 H625"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M785 92 H825"/>
<path class="diagram-link diagram-link-secondary" marker-end="url(#aso-flow-arrow)" d="M705 212 V144"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M910 136 V205"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M810 248 H850"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M930 291 V315"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M850 248 H720 V315"/>
<g class="diagram-node"><rect x="25" y="48" width="160" height="88" rx="12"/><text x="105" y="79" class="diagram-node-title">Bearer Token</text><text x="105" y="106" class="diagram-node-detail">요청 주체 식별</text></g>
<g class="diagram-node"><rect x="225" y="48" width="160" height="88" rx="12"/><text x="305" y="76" class="diagram-node-title">토큰 검증</text><text x="305" y="103" class="diagram-node-detail">set_user_by_bearer</text><text x="305" y="120" class="diagram-node-note">실패 시 context 정리</text></g>
<g class="diagram-node"><rect x="425" y="48" width="160" height="88" rx="12"/><text x="505" y="76" class="diagram-node-title">사용자 context</text><text x="505" y="103" class="diagram-node-detail">CB_AGENT_CTX.USER_ID</text><text x="505" y="120" class="diagram-node-note">EMP_NO · DEPT_CODE</text></g>
<g class="diagram-node diagram-node-accent"><rect x="625" y="48" width="160" height="88" rx="12"/><text x="705" y="76" class="diagram-node-title">마스킹 context</text><text x="705" y="103" class="diagram-node-detail">set_masking_rule_</text><text x="705" y="120" class="diagram-node-detail">context(user_id)</text></g>
<g class="diagram-node diagram-node-accent"><rect x="825" y="48" width="245" height="88" rx="12"/><text x="947" y="76" class="diagram-node-title">컬럼별 세션 변수</text><text x="947" y="103" class="diagram-node-detail">MR_&lt;column_id&gt; = Y / N</text><text x="947" y="120" class="diagram-node-note">현재 DB 세션에만 설정</text></g>
<g class="diagram-node diagram-node-data"><rect x="525" y="212" width="280" height="72" rx="12"/><text x="665" y="240" class="diagram-node-title">마스킹 예외·규칙 메타데이터</text><text x="665" y="263" class="diagram-node-detail">cb_user_masking_rule · cb_column_masking_rule</text></g>
<g class="diagram-node diagram-node-accent"><rect x="850" y="205" width="160" height="86" rx="12"/><text x="930" y="235" class="diagram-node-title">DBMS_REDACT</text><text x="930" y="260" class="diagram-node-detail">SYS_CONTEXT로 MR 읽음</text></g>
<g class="diagram-node diagram-node-data"><rect x="850" y="315" width="160" height="64" rx="12"/><text x="930" y="341" class="diagram-node-title">MR = Y</text><text x="930" y="363" class="diagram-node-detail">원문 표시</text></g>
<g class="diagram-node"><rect x="640" y="315" width="160" height="64" rx="12"/><text x="720" y="341" class="diagram-node-title">MR = N / NULL</text><text x="720" y="363" class="diagram-node-detail">마스킹 적용</text></g>
</svg>
</div>
<p class="form-hint mb-0">연결 고리: 토큰 검증 후 <code>set_masking_rule_context(v_user_id)</code><code>MR_&lt;column_id&gt;</code>를 설정하고, Data Redaction 조건식은 <code>SYS_CONTEXT('CB_AGENT_CTX', 'MR_&lt;column_id&gt;')</code>만 평가합니다. 자세한 SQL은 <a href="/security-sql-scripts?script=aso-masking-runtime">ASO 마스킹 런타임 스크립트</a>에서 확인하세요.</p>
</section>
</details>
</div>
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">행 접근과 분리된 컬럼 제어</span>
<h2>ASO 마스킹 설정 순서</h2>
<p class="section-subtitle">행 접근은 행 접근 규칙과 Oracle VPD 정책이 결정합니다. 이 화면은 허용된 행 안에서 컬럼 값을 원문으로 줄지, 마스킹해서 줄지만 관리합니다.</p>
</div>
</div>
<div class="effective-preview">
<dl>
<div>
<dt>대상 컬럼 등록</dt>
<dd>마스킹 후보 블랙리스트에 DB 컬럼을 올립니다. 이 단계만으로는 값이 마스킹되지 않습니다.</dd>
</div>
<div>
<dt>기본 규칙 연결</dt>
<dd>컬럼에 활성 컬럼 마스킹 규칙을 연결하면 DBMS_REDACT 정책이 동기화됩니다.</dd>
</div>
<div>
<dt>원문 표시 허용</dt>
<dd>예외 사용자는 <a href="/user-masking-rules">컬럼 원문 표시 허용 사용자</a>에서 지정합니다.</dd>
</div>
</dl>
</div>
</section>
<section class="content-band">
<h2>DB ASO 정책 동기화</h2>
<p class="section-subtitle">이제 컬럼 기본 규칙을 연결·해제하거나 연결된 규칙을 활성·비활성화하면 DB 정책도 자동으로 반영됩니다. 아래 버튼은 자동 동기화 도입 전의 기존 설정을 한 번에 보정하거나, 운영 점검 시 현재 설정을 다시 반영할 때만 사용하세요.</p>
<form method="post" action="/masking-rules/synchronize" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<button class="btn btn-outline-primary" type="submit">현재 DB 정책 동기화</button>
</form>
<div class="table-responsive mt-3">
<table class="table table-sm align-middle">
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>레거시 VPD 컬럼 제어</th><th>DB 정책 활성</th><th>상태 판단</th></tr></thead>
<tbody>
<tr th:each="status : ${policyStatuses}">
<td><code th:text="${status.targetLabel()}">POC_2.KB_CUSTOMERS</code></td>
<td><code th:text="${status.policyName()}">KB_CUSTOMER_PII_REDACT</code></td>
<td th:text="${status.configuredColumnCount()}">0</td>
<td th:text="${status.appliedColumnCount()}">0</td>
<td><span class="badge" th:classappend="${status.legacyVpdColumnPolicyCount() == 0} ? ' text-bg-secondary' : ' text-bg-danger'" th:text="${status.legacyVpdColumnPolicyCount() == 0} ? '없음' : ${status.legacyVpdColumnPolicyCount() + '건 활성'}">없음</span></td>
<td><span class="badge" th:classappend="${status.policyEnabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${status.policyEnabled()} ? 'DB 적용 ON' : 'DB 적용 OFF'">DB 적용 OFF</span></td>
<td><span class="badge" th:classappend="${' ' + status.badgeClass()}" th:text="${status.statusLabel()}">미적용</span><br><small class="text-muted" th:text="${status.detail()}">설명</small></td>
</tr>
<tr th:if="${#lists.isEmpty(policyStatuses)}"><td colspan="7" class="text-muted">조회할 ASO 정책이 없습니다.</td></tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<h2>사전 정의 마스킹 방식</h2>
<p class="section-subtitle">임의 SQL이나 정규식을 직접 입력하지 않고 검증된 템플릿만 선택합니다.</p>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>템플릿</th><th>방식</th><th>동작</th><th>ASO 구현</th></tr></thead>
<tbody>
<tr th:each="template : ${templates}">
<td><code th:text="${template.code()}">NULLIFY</code></td>
<td th:text="${template.label()}">값 숨김(NULL)</td>
<td th:text="${template.description()}">설명</td>
<td><code th:text="${template.asoFunction()}">DBMS_REDACT.NULLIFY</code></td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<h2>컬럼 마스킹 규칙 등록</h2>
<p class="section-subtitle">업무용 이름을 붙여 템플릿을 재사용합니다. 예: <code>KB_RRN_STANDARD</code>.</p>
<form method="post" action="/masking-rules" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
규칙 코드
<input class="form-control" name="ruleCode" maxlength="64" pattern="[A-Za-z][A-Za-z0-9_]{2,63}" required placeholder="KB_RRN_STANDARD">
<span class="form-hint">영문·숫자·밑줄만 사용합니다.</span>
</label>
<label>
규칙명
<input class="form-control" name="ruleName" maxlength="100" required placeholder="주민번호 기본 마스킹">
</label>
<label>
마스킹 템플릿
<select class="form-select" name="templateCode" required>
<option th:each="template : ${templates}" th:value="${template.code()}"
th:text="${template.label()}">값 숨김(NULL)</option>
</select>
</label>
<label>
설명
<input class="form-control" name="description" maxlength="400" placeholder="적용 목적 또는 업무 기준">
</label>
<button class="btn btn-primary" type="submit">규칙 등록</button>
</form>
</section>
<section class="content-band">
<h2>등록된 컬럼 마스킹 규칙</h2>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>코드</th><th>규칙명</th><th>템플릿</th><th>설명</th><th>상태</th><th></th></tr></thead>
<tbody>
<tr th:each="rule : ${rules}">
<td><code th:text="${rule.ruleCode()}">KB_RRN_STANDARD</code></td>
<td th:text="${rule.ruleName()}">주민번호 기본 마스킹</td>
<td th:text="${rule.templateLabel()}">값 숨김(NULL)</td>
<td th:text="${rule.description() ?: '-'}">설명</td>
<td><span class="badge" th:classappend="${rule.enabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${rule.enabledYn()}">Y</span></td>
<td>
<form method="post" action="/masking-rules/active" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="ruleId" th:value="${rule.ruleId()}">
<input type="hidden" name="active" th:value="${!rule.enabled()}">
<button class="btn btn-sm btn-outline-secondary" type="submit" th:text="${rule.enabled()} ? '비활성화' : '활성화'">변경</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(rules)}"><td colspan="6" class="text-muted">등록된 컬럼 마스킹 규칙이 없습니다.</td></tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<h2>마스킹 대상 컬럼 추가</h2>
<p class="section-subtitle">DB 실제 컬럼을 ASO 마스킹 후보 블랙리스트에 등록합니다. 이 단계는 컬럼을 선택 목록에 올리는 작업이며, 실제 마스킹 적용은 아래에서 기본 규칙을 연결할 때 수행됩니다.</p>
<form method="post" action="/masking-rules/target-columns" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
대상 객체 / 컬럼
<select class="form-select" name="target" required>
<option value="">선택하세요</option>
<th:block th:each="object : ${maskingTargetObjects}">
<optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(availableMaskingColumnsByObject[object.objectId()])}">
<option th:each="columnName : ${availableMaskingColumnsByObject[object.objectId()]}"
th:value="|${object.objectId()}:${columnName}|"
th:text="${object.displayName() + '.' + columnName}">POC_2.KB_CONTRACTS.PREMIUM</option>
</optgroup>
</th:block>
</select>
<span class="form-hint">현재 관리 대상 ASO 정책이 있는 객체만 표시됩니다. 예: <code>POC_2.KB_CONTRACTS.PREMIUM</code>.</span>
</label>
<button class="btn btn-outline-primary" type="submit">대상 컬럼 추가</button>
</form>
</section>
<section class="content-band">
<h2>민감 컬럼에 기본 규칙 연결</h2>
<p class="section-subtitle">이 목록이 마스킹 대상 블랙리스트입니다. 컬럼 하나에는 활성 규칙 하나만 연결할 수 있으며, 컬럼 원문 표시 허용 사용자는 별도 화면에서 지정합니다. 오른쪽 DB 상태는 컬럼이 속한 객체의 실제 Redaction 정책 상태입니다.</p>
<form method="post" action="/masking-rules/columns" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
민감 컬럼
<select class="form-select" name="columnId" required>
<option value="">선택하세요</option>
<th:block th:each="object : ${objects}">
<optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(sensitiveColumnsByObject[object.objectId()])}">
<option th:each="column : ${sensitiveColumnsByObject[object.objectId()]}"
th:value="${column.columnId()}"
th:text="${column.columnName() + ' [' + column.displayPolicyLabel() + ']'}">RRN_MASKED</option>
</optgroup>
</th:block>
</select>
</label>
<label>
적용할 컬럼 마스킹 규칙
<select class="form-select" name="ruleId" required>
<option value="">선택하세요</option>
<option th:each="rule : ${rules}" th:if="${rule.enabled()}" th:value="${rule.ruleId()}"
th:text="${rule.ruleName() + ' · ' + rule.templateLabel()}">주민번호 기본 마스킹</option>
</select>
</label>
<button class="btn btn-primary" type="submit">컬럼에 연결</button>
</form>
<div class="table-responsive mt-3">
<table class="table table-sm align-middle">
<thead><tr><th>대상 컬럼</th><th>규칙</th><th>템플릿</th><th>백오피스 설정</th><th>DB ASO 적용 상태</th><th></th></tr></thead>
<tbody>
<tr th:each="columnRule : ${columnRules}">
<td><code th:text="${columnRule.targetLabel()}">POC_2.KB_CUSTOMERS.RRN_MASKED</code></td>
<td th:text="${columnRule.ruleName()}">주민번호 기본 마스킹</td>
<td th:text="${columnRule.template().label()}">주민등록번호 부분 마스킹</td>
<td><span class="badge" th:classappend="${columnRule.ruleEnabled()} ? ' text-bg-success' : ' text-bg-warning'" th:text="${columnRule.ruleEnabled()} ? '기본 규칙 연결됨' : '규칙 비활성'">기본 규칙 연결됨</span></td>
<td th:with="policyStatus=${policyStatusByObjectName[columnRule.objectName()]}">
<span class="badge"
th:if="${policyStatus}"
th:classappend="${' ' + policyStatus.badgeClass()}"
th:text="${policyStatus.statusLabel()}">적용됨</span>
<span class="badge text-bg-secondary" th:unless="${policyStatus}">관리 정책 없음</span>
<div class="text-muted small" th:if="${policyStatus}" th:text="${policyStatus.detail()}">상세</div>
</td>
<td>
<form method="post" action="/masking-rules/columns/delete" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="columnId" th:value="${columnRule.columnId()}">
<button class="btn btn-sm btn-outline-danger" type="submit">연결 해제</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(columnRules)}"><td colspan="6" class="text-muted">민감 컬럼에 연결된 컬럼 마스킹 규칙이 없습니다.</td></tr>
</tbody>
</table>
</div>
</section>
</main>
</body>
</html>

View File

@@ -8,7 +8,7 @@
<h1 class="h3">대화형 검색</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>질문을 MCP tool로 라우팅하고, 권한 태그·VPD 행 제한·ORDS 결과를 답변으로 정리합니다.</p>
<p>질문을 MCP tool로 라우팅하고, 권한 태그·행 접근 제한·ORDS 결과를 답변으로 정리합니다.</p>
</details>
</section>
@@ -23,7 +23,7 @@
<p>추천 질문을 선택하거나 질문을 직접 입력하세요.</p>
<details class="explanation-details">
<summary>세션과 조회 결과 설명 보기</summary>
<p>Bearer Token 원문을 붙여 넣거나 사용자를 선택해 10분 검증 세션으로 실제 ORDS/VPD 결과를 조회할 수 있습니다.</p>
<p>Bearer Token 원문을 붙여 넣거나 사용자를 선택해 10분 검증 세션으로 실제 ORDS 행 접근 결과를 조회할 수 있습니다.</p>
</details>
</div>
@@ -56,7 +56,7 @@
<label>
질문
<textarea class="form-control" id="mcp-chat-question" name="question" rows="3"
placeholder="예: BOARD_POSTS에서 이 토큰으로 보이는 행과 NULL 처리 컬럼을 요약해줘." required></textarea>
placeholder="예: BOARD_POSTS에서 이 토큰으로 보이는 행과 ASO 마스킹 컬럼을 요약해줘." required></textarea>
</label>
<div class="question-presets" aria-label="질문 예시">
<button class="btn rw-btn-secondary question-preset" type="button"
@@ -64,15 +64,15 @@
문서 조회 요약
</button>
<button class="btn rw-btn-secondary question-preset" type="button"
data-question="CB_V_SEARCH_DOCUMENTS에서 VPD가 제외한 행과 contents 표시 보호 여부를 구분하고, 보이는 dept_code와 owner_emp_no를 정리해줘.">
data-question="CB_V_SEARCH_DOCUMENTS에서 행 접근 정책이 제외한 행과 contents 표시 보호 여부를 구분하고, 보이는 dept_code와 owner_emp_no를 정리해줘.">
민감 컬럼 확인
</button>
<button class="btn rw-btn-secondary question-preset" type="button"
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 SPRING_BOOT 또는 ORACLE_VPD 태그 권한으로 검색 가능한 검색 단위와 VPD 결과를 요약해줘.">
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 SPRING_BOOT 또는 ORACLE_VPD 태그 권한으로 검색 가능한 검색 단위와 행 접근 결과를 요약해줘.">
태그 벡터 검색
</button>
<button class="btn rw-btn-secondary question-preset" type="button"
data-question="질문과 가장 가까운 ORDS/VPD MCP tool을 선택하고, 선택 근거와 필요한 bearer token 흐름만 보여줘.">
data-question="질문과 가장 가까운 ORDS 행 접근 MCP tool을 선택하고, 선택 근거와 필요한 bearer token 흐름만 보여줘.">
라우팅만 확인
</button>
</div>

View File

@@ -8,7 +8,7 @@
<h1>검색 해석</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>질문을 근거로 MCP tool을 고르고, Bearer Token으로 VPD/ORDS 결과를 조회한 뒤 모델이 설명합니다.</p>
<p>질문을 근거로 MCP tool을 고르고, Bearer Token으로 ORDS 행 접근 결과를 조회한 뒤 모델이 설명합니다.</p>
</details>
</div>
@@ -55,11 +55,11 @@
<label class="span-2">
질문
<textarea class="form-control" id="mcp-reasoning-question" name="question" rows="3"
placeholder="비워도 조회 행, NULL 처리, 권한 범위, 다음 조치를 요약합니다."></textarea>
placeholder="비워도 조회 행, ASO 마스킹 컬럼, 권한 범위, 다음 조치를 요약합니다."></textarea>
</label>
<div class="question-presets span-2" aria-label="질문 예시">
<button class="btn rw-btn-secondary question-preset" type="button"
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 이 토큰이 볼 수 있는 기술 태그 검색 단위 수와 VPD가 제외한 범위를 요약해줘.">
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 이 토큰이 볼 수 있는 기술 태그 검색 단위 수와 행 접근 정책이 제외한 범위를 요약해줘.">
기본 분석
</button>
<button class="btn rw-btn-secondary question-preset" type="button"
@@ -71,7 +71,7 @@
태그 권한 범위 판단
</button>
<button class="btn rw-btn-secondary question-preset" type="button"
data-question="ORDS path, VPD policy, TAG permission, token 상태 중 이상 징후를 먼저 bullet로 요약하고 다음 확인 순서를 제시해줘.">
data-question="ORDS path, 행 접근 policy, TAG permission, token 상태 중 이상 징후를 먼저 bullet로 요약하고 다음 확인 순서를 제시해줘.">
운영 점검 요약
</button>
</div>
@@ -97,7 +97,7 @@
<tr th:each="tool : ${tools}">
<td><code th:text="${tool.name()}">ords.query.admin.board_posts</code></td>
<td><code th:text="${tool.ordsPath()}">path</code></td>
<td th:text="${tool.description()}">VPD/ORDS 조회 도구</td>
<td th:text="${tool.description()}">ORDS 행 접근 조회 도구</td>
</tr>
<tr th:if="${#lists.isEmpty(tools)}">
<td colspan="3" class="text-muted">등록된 보호 객체 도구가 없습니다.</td>

View File

@@ -8,7 +8,7 @@
<h1>MCP 연동</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>보호 객체 ORDS 조회를 외부 MCP client에서 호출할 수 있도록 context path별 SSE transport로 제공합니다.</p>
<p>사용자 Bearer Token을 전달해 GPT-5.4-mini Select AI 자연어 조회를 실행하는 단일 MCP tool을 제공합니다. Select AI는 comment, annotation, constraint 메타데이터를 함께 사용하고, DB에서는 토큰 기준 행 접근 context가 적용됩니다.</p>
</details>
</section>
@@ -67,7 +67,7 @@
</tr>
<tr>
<th>Auth</th>
<td><code>Basic Auth</code> / <code>BACKOFFICE_ADMIN_USER</code>, <code>BACKOFFICE_ADMIN_PASSWORD</code></td>
<td><code>Authorization: Bearer &lt;사용자 Bearer Token&gt;</code> — 이 토큰 하나로 행 접근 컨텍스트를 설정합니다.</td>
</tr>
<tr>
<th>Methods</th>
@@ -97,12 +97,9 @@
<td th:text="${tool.displayName()}">ADMIN.BOARD_POSTS</td>
<td><code th:text="${tool.ordsPath()}">cb-ords/cb-object-query/admin/board_posts</code></td>
<td>
<div th:text="${tool.description()}">VPD/ORDS 조회 도구 설명</div>
<div th:text="${tool.description()}">ORDS 행 접근 조회 도구 설명</div>
<small class="text-muted">
<code>bearerToken</code>Authorization · <code>limit</code>ORDS limit · object → 이 도구에 고정
<span th:if="${tool.displayName().toUpperCase().endsWith('CB_VECTOR_SEARCH_DOCUMENTS')}">
· <code>embedding[]</code> → 검색 JSON 본문
</span>
HTTP <code>Authorization</code>행 접근 컨텍스트 · <code>prompt</code>GPT-5.4-mini Select AI 자연어 질의 · <code>limit</code> → 최대 반환 행 수
</small>
</td>
</tr>
@@ -119,10 +116,10 @@
<summary>tools/call parameter 예시 보기</summary>
<h2>tools/call Arguments</h2>
<pre class="code-block">{
"bearerToken": "vpd_live_xxx",
"prompt": "KB_CLAIMS의 전체 청구 건수를 조회해 줘.",
"limit": 50
}</pre>
<p class="form-hint">벡터 검색 tool <code>CB_VECTOR_SEARCH_DOCUMENTS</code>는 여기에 외부 임베딩 모델이 만든 <code>embedding</code> 숫자 배열을 추가합니다. 토큰·limit은 모든 tool에 공통이고, embedding은 벡터 tool에만 필요합니다.</p>
<p class="form-hint">등록 tool <code>ords.query.kb_select_ai_vpd</code> 하나입니다. HTTP Authorization의 사용자 Bearer Token으로 컨텍스트를 설정한 뒤 GPT-5.4-mini가 comment, annotation, constraint를 참고해 생성한 검증된 읽기 전용 KB 원장 SQL만 실행합니다.</p>
</details>
</section>
@@ -156,9 +153,9 @@
"id": 3,
"method": "tools/call",
"params": {
"name": "ords.query.admin.board_posts",
"name": "ords.query.kb_select_ai_vpd",
"arguments": {
"bearerToken": "vpd_live_xxx",
"prompt": "KB_CLAIMS의 전체 청구 건수를 조회해 줘.",
"limit": 50
}
}

View File

@@ -8,7 +8,7 @@
<h1>조회 대상</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>이 화면은 DB 객체를 HTTP로 연결할 경로만 등록합니다. 누가 어떤 행을 볼 수 있는지는 권한 규칙과 VPD에서 결정합니다.</p>
<p>이 화면은 DB 객체를 HTTP로 연결할 경로만 등록합니다. 누가 어떤 행을 볼 수 있는지는 행 접근 규칙과 Oracle VPD 정책에서 결정합니다.</p>
</details>
</div>
@@ -16,7 +16,7 @@
<strong>지식 검색 연결:</strong> 문서 검색 단위와 기술 태그를 등록한 뒤 전용 ORDS 검색을 연결합니다.
<details class="explanation-details mt-2">
<summary>지식 검색 연결 흐름 보기</summary>
<p>문서를 검색 단위로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다. 그 다음 <a href="/permissions">권한 관리</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면 태그 중 하나라도 맞는 검색 단위만 검색됩니다.</p>
<p>문서를 검색 단위로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다. 그 다음 <a href="/permissions">행 접근 규칙</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면 태그 중 하나라도 맞는 검색 단위만 검색됩니다.</p>
</details>
</div>
@@ -28,7 +28,7 @@
<p class="section-subtitle">DB 객체 하나를 하나의 조회 Handler로 연결하는 기본 구성입니다.</p>
<details class="explanation-details">
<summary>대상 추가 후 생성되는 내용 보기</summary>
<p>대상 추가 후 Handler를 생성하면 VPD context 설정과 기본 SELECT가 포함된 PL/SQL이 만들어집니다. 소스 보기에서 실제 업무에 맞게 수정할 수 있습니다.</p>
<p>대상 추가 후 Handler를 생성하면 행 접근 context 설정과 기본 SELECT가 포함된 PL/SQL이 만들어집니다. 소스 보기에서 실제 업무에 맞게 수정할 수 있습니다.</p>
</details>
<form method="post" action="/objects" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
@@ -66,7 +66,7 @@
</form>
<details class="explanation-details explanation-warning mt-3">
<summary>기본 Handler 구성과 수정 가이드 보기</summary>
<p><code>cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id)</code>로 토큰의 사용자·역할 컨텍스트와 검증 요청 식별자를 넣고, 선택한 한 테이블에 <code>SELECT ... FROM OWNER.TABLE</code>을 실행한 뒤 JSON으로 반환합니다. 이것은 유일한 사용 방식이 아니라 시작점이며, Handler 소스와 ORDS 메타데이터에서 수정할 수 있습니다. 컬럼 민감도·마스킹은 이 화면에서 다루지 않고 <a href="/permissions">권한 관리의 원문 표시 허용 컬럼</a>에서 별도로 설정합니다.</p>
<p><code>cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id)</code>로 토큰의 사용자·역할 컨텍스트와 검증 요청 식별자를 넣고, 선택한 한 테이블에 <code>SELECT ... FROM OWNER.TABLE</code>을 실행한 뒤 JSON으로 반환합니다. 이것은 유일한 사용 방식이 아니라 시작점이며, Handler 소스와 ORDS 메타데이터에서 수정할 수 있습니다. 컬럼 민감도·마스킹은 이 화면에서 다루지 않고 <a href="/masking-rules">컬럼 마스킹</a>에서 별도로 설정합니다.</p>
</details>
</section>
@@ -141,8 +141,8 @@
<details class="explanation-details">
<summary>이 대상의 자동 연결 흐름 보기</summary>
<div class="object-handler-explainer">
<strong>토큰 헤더 → VPD context 설정 → 선택한 단일 테이블의 기본 SELECT → JSON 응답</strong>
<small>행 접근은 권한 규칙/VPD가 담당합니다. 원문 표시 예외는 권한 관리 Step 4에서 여러 컬럼을 등록하세요.</small>
<strong>토큰 헤더 → 행 접근 context 설정 → 선택한 단일 테이블의 기본 SELECT → JSON 응답</strong>
<small>행 접근은 행 접근 규칙과 Oracle VPD 정책이 담당합니다. 컬럼 원문/마스킹은 컬럼 마스킹의 ASO/Data Redaction 설정에서 관리하세요.</small>
</div>
</details>
<div th:if="${object.objectName() != 'CB_VECTOR_SEARCH_DOCUMENTS'}"

View File

@@ -8,11 +8,39 @@
<h1>운영 현황</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>보호 객체별 ORDS handler, VPD policy, 권한 rule, 최근 검증 상태를 확인합니다. 이상이 있을 때는 해당 객체의 권한·VPD·ORDS 순서로 점검하세요.</p>
<p>보호 객체별 ORDS handler, 행 접근 정책(VPD), 행 접근 rule, 최근 검증 상태를 확인합니다. 이상이 있을 때는 해당 객체의 행 접근 규칙·VPD 정책·ORDS 순서로 점검하세요.</p>
<p>컬럼 마스킹은 ASO/Data Redaction 정책 상태로 별도 확인합니다. 행 접근 정책이 정상이더라도 ASO 정책이 불일치하면 민감 컬럼이 예상과 다르게 원문 또는 마스킹값으로 반환될 수 있습니다.</p>
</details>
</div>
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">운영 판단 기준</span>
<h2>행 접근 정책(VPD)과 컬럼 마스킹 정책(ASO)을 분리해서 봅니다</h2>
<p class="section-subtitle">VPD는 행 접근, ASO/Data Redaction은 컬럼 원문/마스킹을 담당합니다. 둘 중 하나만 정상이어도 전체 권한 결과가 정상이라고 판단하면 안 됩니다.</p>
</div>
</div>
<div class="effective-preview">
<dl>
<div>
<dt>행 접근 정책(VPD)</dt>
<dd>아래 보호 객체 표에서 policy, function, 최근 접근 검증 상태를 확인합니다.</dd>
</div>
<div>
<dt>ASO 컬럼 정책</dt>
<dd>아래 ASO 표에서 백오피스 활성 컬럼 수와 DB Redaction 컬럼 수가 일치하는지 확인합니다.</dd>
</div>
<div>
<dt>실제 결과 검증</dt>
<dd>상태가 정상이어도 최종 판단은 <a href="/probe">접근 검증</a>에서 토큰 기준 응답으로 확인합니다.</dd>
</div>
</dl>
</div>
</section>
<section class="content-band">
<h2>행 접근 정책(VPD) · ORDS 검증 상태</h2>
<div class="table-responsive">
<table class="table table-sm align-middle operation-status-table">
<thead>
@@ -75,6 +103,34 @@
</table>
</div>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>ASO 컬럼 마스킹 정책 상태</h2>
<p class="section-subtitle">컬럼 마스킹 화면의 백오피스 설정과 실제 Oracle Data Redaction 정책을 비교한 결과입니다.</p>
</div>
<a class="btn btn-sm rw-btn-secondary" href="/masking-rules">컬럼 마스킹 열기</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>상태</th><th>확인 결과</th></tr></thead>
<tbody>
<tr th:each="status : ${maskingPolicyStatuses}">
<td><code th:text="${status.targetLabel()}">POC_2.KB_CLAIMS</code></td>
<td><code th:text="${status.policyName()}">KB_CLAIM_AMOUNT_REDACT</code></td>
<td th:text="${status.configuredColumnCount()}">0</td>
<td th:text="${status.appliedColumnCount()}">0</td>
<td><span class="badge" th:classappend="${' ' + status.badgeClass()}" th:text="${status.statusLabel()}">적용됨</span></td>
<td th:text="${status.detail()}">활성 규칙과 DB 정책 컬럼이 일치합니다.</td>
</tr>
<tr th:if="${#lists.isEmpty(maskingPolicyStatuses)}">
<td colspan="6" class="text-muted">조회할 ASO 정책 상태가 없습니다.</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</body>
</html>

View File

@@ -8,12 +8,12 @@
<h1>조회 연동</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>Bearer Token을 DB 컨텍스트로 변환한 뒤 VPD 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다. 실행 결과의 SQL_ID 증적과 권한 조건 재현은 접근 검증 메뉴에서 확인할 수 있습니다.</p>
<p>Bearer Token을 DB 컨텍스트로 변환한 뒤 행 접근 정책(VPD)이 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다. 실행 결과의 SQL_ID 증적과 조건 재현은 접근 검증 메뉴에서 확인할 수 있습니다.</p>
</details>
</div>
<div class="alert alert-info">
권한별로 VPD가 붙인 실제 행 조건을 확인하려면 <a href="/probe">접근 검증</a>에서 이 Handler의 보호 객체를 실행하세요. SQL trace를 반환하도록 갱신된 Handler는 같은 ORDS 세션의 <code>CB_AGENT_DOC_VPD_FILTER</code> predicate를 함께 표시합니다.
사용자별로 적용된 실제 행 조건을 확인하려면 <a href="/probe">접근 검증</a>에서 이 Handler의 보호 객체를 실행하세요. SQL trace를 반환하도록 갱신된 Handler는 같은 ORDS 세션의 <code>CB_AGENT_DOC_VPD_FILTER</code> predicate를 함께 표시합니다.
</div>
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>

View File

@@ -1,14 +1,15 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('접근 규칙')}"></head>
<head th:replace="~{fragments/layout :: head('접근 규칙')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<div class="page-title">
<h1>접근 규칙</h1>
<h1>접근 규칙</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>VPD policy function이 참조할 사용자, 역할, 행 규칙, 컬럼 원문 허용 규칙을 저장합니다. KB 조건 코드는 토큰 context 값으로 치환되고, 정적 SQL 조건은 대상 객체의 WHERE 절에 그대로 추가됩니다. 한 권한의 규칙은 AND, 서로 다른 ALLOW 권한은 OR로 합치며 DENY는 허용 결과에서 제외합니다.</p>
<p>이 화면은 <strong>행 접근 규칙</strong>만 저장합니다. 저장값은 최종 SQL이 아니라 <code>CB_AGENT_DOC_VPD_FILTER</code>가 읽어서 대상 테이블의 WHERE predicate로 바꾸는 매핑 데이터입니다.</p>
<p>컬럼 원문/마스킹은 이 화면에서 처리하지 않습니다. 주민번호, 보험료, 청구금액 같은 컬럼 표시는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 정책에서 관리합니다.</p>
</details>
</div>
@@ -18,15 +19,14 @@
<section class="content-band">
<div class="section-heading">
<div>
<h2>새 접근 규칙</h2>
<p class="section-description">저장 전 적용 대상과 행 범위를 검토한 뒤 VPD 권한을 반영합니다.</p>
<h2> 접근 규칙</h2>
<p class="section-description">저장 전 적용 대상과 행 범위를 검토한 뒤 행 접근 규칙을 반영합니다.</p>
</div>
<div class="wizard-progress" aria-label="권한 추가 단계">
<button class="wizard-step-indicator active" type="button" data-wizard-target="1">역할</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="2">대상</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="3">접근 범위</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="4">표시 예외</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="5">검토</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="4">검토</button>
</div>
</div>
<form method="post" action="/permissions" class="permission-wizard" data-permission-wizard>
@@ -138,7 +138,7 @@
<div class="wizard-panel-heading">
<div>
<h3>권한 효과와 행 규칙</h3>
<p>허용/거부 방향과 VPD 행 필터 조건을 설정합니다.</p>
<p>허용/거부 방향과 행 필터 조건을 설정합니다.</p>
</div>
</div>
<label>
@@ -179,40 +179,17 @@
</div>
<details class="explanation-details">
<summary>행 규칙의 두 가지 적용 방식 보기</summary>
<p class="wizard-hint"><strong>조건 코드</strong>는 토큰 context로 치환됩니다. 예를 들어 <code>본인 담당 고객 / CUST_ID</code>는 계약원장에서 토큰 사용자 ID의 담당 고객을 찾아 현재 객체의 <code>CUST_ID</code>에 적용합니다. <strong>정적 SQL 조건식</strong><code>CONTRACT_STATUS = '정상'</code>처럼 현재 객체 컬럼을 사용한 WHERE 절을 그대로 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다. 역할명은 VPD WHERE 절에 직접 들어가지 않습니다.</p>
<p class="wizard-hint"><strong>조건 코드</strong>는 토큰 context로 치환됩니다. 예를 들어 <code>본인 담당 고객 / CUST_ID</code>는 계약원장에서 토큰 사용자 ID의 담당 고객을 찾아 현재 객체의 <code>CUST_ID</code>에 적용합니다. <strong>정적 SQL 조건식</strong><code>CONTRACT_STATUS = '정상'</code>처럼 현재 객체 컬럼을 사용한 WHERE 절을 그대로 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다. 역할명은 최종 WHERE 절에 직접 들어가지 않습니다.</p>
<p class="wizard-hint"><strong>컬럼 원문/마스킹은 제외했습니다.</strong> 행 접근 필터는 행만 남기고, ASO/Data Redaction이 허용된 행 안에서 컬럼을 원문 또는 마스킹으로 반환합니다.</p>
</details>
</div>
</div>
<div class="wizard-panel" data-wizard-step="4">
<div class="wizard-panel-heading">
<div>
<h3>권한별 원문 표시 예외</h3>
<p>이 역할이 이미 볼 수 있는 행에서 마스킹을 제외할 컬럼을 여러 개 선택합니다.</p>
</div>
</div>
<div class="masking-column-picker">
<div class="field-block-title">선택 가능한 표시 보호 컬럼</div>
<div class="question-presets" data-maskable-column-list>
<span class="text-muted small">보호 객체를 선택하면 마스킹 대상 컬럼이 표시됩니다.</span>
</div>
</div>
<label>
원문 표시 허용(마스킹 제외) 컬럼 · 여러 개 가능
<input class="form-control" name="visibleColumns" placeholder="예: CONTENTS, SOURCE_URI">
<span class="selected-column-list" data-selected-visible-columns aria-live="polite"></span>
</label>
<details class="explanation-details">
<summary>원문 표시 허용 컬럼 안내 보기</summary>
<p class="wizard-hint">쉼표로 여러 컬럼을 등록하거나 위 버튼을 여러 번 누르세요. 등록하지 않은 민감 컬럼은 기본 표시 보호를 유지합니다. 행을 볼 수 있는지는 이 목록이 아니라 권한 규칙과 VPD가 결정합니다.</p>
</details>
</div>
<div class="wizard-panel" data-wizard-step="5">
<div class="wizard-panel-heading">
<div>
<h3>저장 전 검토</h3>
<p>저장될 권한과 예상 effective policy를 확인합니다.</p>
<p>저장될 행 접근 규칙과 VPD predicate 변환 예시를 확인합니다. 컬럼 마스킹은 ASO 화면에서 별도로 설정합니다.</p>
</div>
</div>
<div class="policy-preview" data-policy-preview>
@@ -222,10 +199,9 @@
<div><dt>보호 객체</dt><dd data-preview="object">-</dd></div>
<div><dt>적용 대상</dt><dd data-preview="affectedPrincipals">-</dd></div>
<div><dt>객체 컬럼</dt><dd data-preview="objectColumnsFinal">-</dd></div>
<div><dt>행 접근</dt><dd data-preview="rowPolicy">-</dd></div>
<div><dt>행 접근 업무 의미</dt><dd data-preview="rowPolicy">-</dd></div>
<div><dt>VPD predicate 예상</dt><dd data-preview="predicatePreview">-</dd></div>
<div><dt>권한별 컬럼 마스킹</dt><dd data-preview="columnPolicy">-</dd></div>
<div><dt>NULL 처리 예상</dt><dd data-preview="nullPolicy">-</dd></div>
<div><dt>컬럼 마스킹</dt><dd data-preview="columnPolicy">-</dd></div>
<div class="policy-preview-emphasis"><dt>저장 영향</dt><dd data-preview="saveGuard">-</dd></div>
<div><dt>되돌리기</dt><dd>저장 후 아래 권한 목록에서 삭제할 수 있습니다. 이 객체의 마지막 권한을 삭제하면 보호 객체가 비활성화될 수 있습니다.</dd></div>
</dl>
@@ -241,8 +217,8 @@
</section>
<section class="content-band">
<h2>접근 규칙 목록</h2>
<p class="section-subtitle">한 접근 규칙 안에서는 조건 코드와 정적 SQL 조건을 AND로 합쳐 범위를 좁힙니다. 같은 보호 대상의 서로 다른 ALLOW 권한은 OR, DENY 권한은 허용 결과에서 제외됩니다.</p>
<h2>접근 규칙 목록</h2>
<p class="section-subtitle">업무 의미를 먼저 보고, 세부 저장값과 VPD predicate 변환 예시는 행의 상세에서 확인하세요. 컬럼 원문/마스킹은 컬럼 마스킹에서 관리합니다.</p>
<div class="table-responsive">
<table class="table table-sm align-middle permission-list-table">
<thead>
@@ -250,8 +226,8 @@
<th>역할</th>
<th>보호 대상</th>
<th>효과</th>
<th>접근 범위</th>
<th>표시 예외</th>
<th>업무 접근 범위</th>
<th>컬럼 마스킹</th>
<th>관리</th>
</tr>
</thead>
@@ -265,11 +241,20 @@
th:classappend="${permission.permissionEffect() == 'DENY'} ? ' text-bg-danger' : ' text-bg-success'"
th:text="${permission.permissionEffect()}">ALLOW</span>
</td>
<td><pre class="table-pre" th:text="${permission.filterPreview() ?: permission.rules() ?: '-'}">ALL ROWS</pre></td>
<td th:text="${permission.visibleColumns()} ?: '-'">CONTENTS</td>
<td><pre class="table-pre" th:text="${permission.businessRuleSummary()}">전체 행</pre></td>
<td>
<span class="badge text-bg-light">ASO 화면에서 관리</span>
<div class="text-muted small" th:text="${permission.columnControlSummary()}">컬럼 원문/마스킹은 컬럼 마스킹에서 관리</div>
</td>
<td>
<details class="row-management">
<summary>관리</summary>
<div class="delete-impact">
<span>저장 규칙</span>
<pre class="table-pre mt-1" th:text="${permission.storedRuleSummary()}">CUST_ID OWN_CUSTOMER</pre>
<span>VPD predicate 변환 예시</span>
<pre class="table-pre mt-1" th:text="${permission.vpdMappingSummary()}">EXISTS (...)</pre>
</div>
<div class="delete-impact">
<span>이 역할은 이 대상의 SELECT 권한을 잃습니다.</span>
<strong th:text="${permission.roleName() + ' → ' + permission.objectName()}">ROLE → OBJECT</strong>
@@ -289,7 +274,7 @@
</td>
</tr>
<tr th:if="${#lists.isEmpty(permissions)}">
<td colspan="6" class="text-muted">등록된 접근 규칙이 없습니다.</td>
<td colspan="6" class="text-muted">등록된 접근 규칙이 없습니다.</td>
</tr>
</tbody>
</table>

View File

@@ -19,7 +19,7 @@
<p>토큰과 대상 객체를 입력하면 실제 조회 결과를 보여줍니다.</p>
<details class="explanation-details">
<summary>권한 판정 과정 보기</summary>
<p>토큰은 사용자를 찾는 열쇠입니다. 서버가 직접 역할과 그룹 상속 역할을 합치고, VPD 저장된 행·열 규칙을 적용한 결과를 보여줍니다.</p>
<p>토큰은 사용자를 찾는 열쇠입니다. 서버가 직접 역할과 그룹 상속 역할을 합치고, VPD 저장된 행 규칙을 적용합니다. 컬럼 원문/마스킹은 ASO/Data Redaction 결과로 별도 확인합니다.</p>
</details>
</div>
<a class="btn rw-btn-secondary" href="/tokens">검증 세션이 없나요? 먼저 발급하기</a>

View File

@@ -8,8 +8,7 @@
<h1>역할</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p th:if="${backofficeTrack == 'DDS'}">역할은 사용자와 접근 태그 규칙을 연결하는 기준입니다. 실제 접근 범위는 접근 규칙에서 관리합니다.</p>
<p th:unless="${backofficeTrack == 'DDS'}">사용자에게 부여할 역할을 관리합니다. 표시 보호 등급 상한은 허용된 행에서 원문으로 볼 수 있는 컬럼 등급의 참고값이며, 행 접근 권한은 권한 규칙과 VPD에서 결정합니다.</p>
<p>사용자에게 부여할 역할을 관리합니다. 표시 보호 등급 상한은 허용된 행에서 원문으로 볼 수 있는 컬럼 등급의 참고값이며, 행 접근 권한은 행 접근 규칙과 Oracle VPD 정책에서 결정합니다.</p>
</details>
</div>
@@ -20,8 +19,7 @@
<h2>역할 추가</h2>
<form method="post" action="/roles" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input th:if="${backofficeTrack == 'DDS'}" type="hidden" name="maxSensitivityLevel" value="PUBLIC">
<label th:unless="${backofficeTrack == 'DDS'}">
<label>
역할명
<input class="form-control" name="roleName" placeholder="HR_DEPT_ROLE" required>
</label>
@@ -37,7 +35,7 @@
<option value="CONFIDENTIAL">CONFIDENTIAL</option>
<option value="RESTRICTED">RESTRICTED</option>
</select>
<span class="form-hint">이 역할이 허용된 행에서 원문으로 볼 수 있는 컬럼 등급의 참고 상한입니다. 행을 볼 수 있는지는 권한 규칙과 VPD가 결정하며, 이 값은 행 권한을 부여하지 않습니다.</span>
<span class="form-hint">이 역할이 허용된 행에서 원문으로 볼 수 있는 컬럼 등급의 참고 상한입니다. 행을 볼 수 있는지는 행 접근 규칙과 Oracle VPD 정책이 결정하며, 이 값은 행 권한을 부여하지 않습니다.</span>
</label>
<button class="btn rw-btn-primary" type="submit">추가</button>
</form>
@@ -51,7 +49,7 @@
<tr>
<th>ID</th>
<th>역할명</th>
<th th:unless="${backofficeTrack == 'DDS'}">표시 보호 등급 상한</th>
<th>표시 보호 등급 상한</th>
<th>삭제 영향</th>
<th></th>
</tr>
@@ -62,7 +60,7 @@
hasDependencies=${impact != null && (!#lists.isEmpty(impact.directUsers()) || !#lists.isEmpty(impact.groups()) || impact.permissionCount() > 0)}">
<td th:text="${role.roleId()}">10</td>
<td th:text="${role.roleName()}">HR_DEPT_ROLE</td>
<td th:unless="${backofficeTrack == 'DDS'}">
<td>
<span class="badge text-bg-light" th:title="${'허용된 행의 컬럼 표시 상한: ' + role.maxSensitivityLevel()}"
th:text="${role.maxSensitivityLevel()}">PUBLIC</span>
<form method="post" action="/roles/max-sensitivity" class="inline-form">
@@ -113,7 +111,7 @@
</td>
</tr>
<tr th:if="${#lists.isEmpty(roles)}">
<td th:colspan="${backofficeTrack == 'DDS'} ? 4 : 5" class="text-muted">등록된 역할이 없습니다.</td>
<td colspan="5" class="text-muted">등록된 역할이 없습니다.</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,198 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('DB 메타데이터')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<section class="page-title">
<span class="badge text-bg-primary">Select AI 근거 메타데이터</span>
<h1 class="mt-2">DB 메타데이터</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>
<code>POC_2</code> KB 업무 테이블의 table/column comment와 Oracle annotation을 조회·수정합니다.
Select AI profile의 <code>comments=true</code>, <code>annotations=true</code> 설정에서는 이 값들이 SQL 생성 근거로 들어갑니다.
</p>
<p class="mb-0">임의 스키마나 임의 테이블은 수정하지 않고, 백오피스가 승인한 7개 업무 테이블만 대상으로 합니다.</p>
</details>
</section>
<section class="alert alert-success" th:if="${message}" th:text="${message}">저장 완료</section>
<section class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}">오류</section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>테이블 선택</h2>
<p class="section-subtitle">정형 MCP/Select AI가 참조하는 KB 업무 원장 7개만 표시합니다.</p>
</div>
<span class="badge text-bg-secondary" th:text="${#lists.size(tables)}">7</span>
</div>
<div class="structured-table-grid">
<a th:each="entry : ${tables}"
class="structured-table-card"
th:classappend="${entry.key() == selectedKey} ? ' is-selected'"
th:href="@{/schema-metadata(table=${entry.key()})}">
<strong th:text="${entry.businessName()}">고객원장</strong>
<code th:text="${entry.tableName()}">KB_CUSTOMERS</code>
<small th:text="${entry.description()}">고객 기본정보</small>
</a>
</div>
</section>
<section class="content-band" th:if="${metadata}">
<div class="section-heading">
<div>
<span class="badge text-bg-secondary">TABLE</span>
<h2 class="mt-2" th:text="${metadata.table().businessName()}">계약원장</h2>
<p class="section-subtitle">
<code th:text="${'POC_2.' + metadata.table().tableName()}">POC_2.KB_CONTRACTS</code>
<span th:text="${' · ' + metadata.table().description()}"> · 설명</span>
</p>
</div>
<a class="btn btn-sm rw-btn-secondary" th:href="@{/structured-data(table=${metadata.table().key()})}">데이터 미리보기</a>
</div>
<form method="post" action="/schema-metadata/table-comment" class="mb-4">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="table" th:value="${metadata.table().key()}">
<label class="form-label fw-semibold">Table comment</label>
<textarea class="form-control" rows="3" name="comment" maxlength="4000"
th:text="${metadata.tableComment()}"></textarea>
<div class="form-hint mt-1">Oracle <code>COMMENT ON TABLE</code>로 즉시 반영됩니다. 빈 값으로 저장하면 comment를 비웁니다.</div>
<button class="btn rw-btn-primary mt-2" type="submit">테이블 comment 저장</button>
</form>
<div class="section-heading">
<div>
<h3>Table annotations</h3>
<p class="section-subtitle">기존 annotation은 값 수정 또는 빈 값 저장으로 삭제할 수 있습니다.</p>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th style="width: 220px;">Annotation</th><th>Value</th><th style="width: 110px;"></th></tr></thead>
<tbody>
<tr th:each="annotation : ${metadata.tableAnnotations()}">
<td><code th:text="${annotation.name()}">DISPLAY_NAME</code></td>
<td>
<form method="post" action="/schema-metadata/table-annotation" class="metadata-inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="table" th:value="${metadata.table().key()}">
<input type="hidden" name="annotationName" th:value="${annotation.name()}">
<textarea class="form-control" rows="2" name="annotationValue" maxlength="4000"
th:text="${annotation.value()}"></textarea>
<div class="form-hint mt-1">저장 전 기존 같은 key는 DROP 후 다시 ADD합니다.</div>
<button class="btn btn-sm rw-btn-primary mt-2" type="submit">저장</button>
</form>
</td>
<td class="text-muted">기존</td>
</tr>
<tr th:if="${#lists.isEmpty(metadata.tableAnnotations())}">
<td colspan="3" class="text-muted">등록된 table annotation이 없습니다.</td>
</tr>
<tr>
<td colspan="3">
<form method="post" action="/schema-metadata/table-annotation" class="row g-2 align-items-start">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="table" th:value="${metadata.table().key()}">
<div class="col-md-3">
<input class="form-control" name="annotationName" placeholder="예: QUERY_HINT" maxlength="128">
</div>
<div class="col-md-7">
<textarea class="form-control" rows="2" name="annotationValue" maxlength="4000"
placeholder="Select AI가 참고할 업무 의미, 조인 힌트, 값 도메인 등을 입력"></textarea>
</div>
<div class="col-md-2">
<button class="btn rw-btn-secondary w-100" type="submit">추가</button>
</div>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="content-band" th:if="${metadata}">
<div class="section-heading">
<div>
<h2>Column comments / annotations</h2>
<p class="section-subtitle">컬럼 comment와 컬럼 annotation을 수정합니다. 저장 즉시 DB dictionary에 반영됩니다.</p>
</div>
<span class="badge text-bg-secondary" th:text="${#lists.size(metadata.columns()) + '개 컬럼'}">0개 컬럼</span>
</div>
<div class="metadata-column-stack">
<details class="content-band nested-content-band"
th:each="column : ${metadata.columns()}"
th:open="${!#lists.isEmpty(column.annotations())}">
<summary class="d-flex justify-content-between align-items-center gap-3">
<span>
<code th:text="${column.columnName()}">CONTRACT_NO</code>
<small class="text-muted ms-2" th:text="${column.dataType()}">VARCHAR2(30)</small>
<span class="badge text-bg-light ms-2" th:text="${column.nullable()} ? 'NULL 허용' : 'NOT NULL'">NOT NULL</span>
</span>
<span class="text-muted" th:text="${#lists.size(column.annotations()) + ' annotations'}">0 annotations</span>
</summary>
<form method="post" action="/schema-metadata/column-comment" class="mt-3">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="table" th:value="${metadata.table().key()}">
<input type="hidden" name="column" th:value="${column.columnName()}">
<label class="form-label fw-semibold">Column comment</label>
<textarea class="form-control" rows="2" name="comment" maxlength="4000"
th:text="${column.comment()}"></textarea>
<button class="btn btn-sm rw-btn-primary mt-2" type="submit">컬럼 comment 저장</button>
</form>
<div class="table-responsive mt-3">
<table class="table table-sm align-middle">
<thead><tr><th style="width: 220px;">Annotation</th><th>Value</th><th style="width: 110px;"></th></tr></thead>
<tbody>
<tr th:each="annotation : ${column.annotations()}">
<td><code th:text="${annotation.name()}">DISPLAY_NAME</code></td>
<td>
<form method="post" action="/schema-metadata/column-annotation">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="table" th:value="${metadata.table().key()}">
<input type="hidden" name="column" th:value="${column.columnName()}">
<input type="hidden" name="annotationName" th:value="${annotation.name()}">
<textarea class="form-control" rows="2" name="annotationValue" maxlength="4000"
th:text="${annotation.value()}"></textarea>
<button class="btn btn-sm rw-btn-primary mt-2" type="submit">저장</button>
</form>
</td>
<td class="text-muted">기존</td>
</tr>
<tr th:if="${#lists.isEmpty(column.annotations())}">
<td colspan="3" class="text-muted">등록된 column annotation이 없습니다.</td>
</tr>
<tr>
<td colspan="3">
<form method="post" action="/schema-metadata/column-annotation" class="row g-2 align-items-start">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="table" th:value="${metadata.table().key()}">
<input type="hidden" name="column" th:value="${column.columnName()}">
<div class="col-md-3">
<input class="form-control" name="annotationName" placeholder="예: VALUE_DOMAIN" maxlength="128">
</div>
<div class="col-md-7">
<textarea class="form-control" rows="2" name="annotationValue" maxlength="4000"
placeholder="컬럼 표시명, 값 범위, 조인 대상, 민감도 등을 입력"></textarea>
</div>
<div class="col-md-2">
<button class="btn rw-btn-secondary w-100" type="submit">추가</button>
</div>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</details>
</div>
</section>
</main>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('보안 SQL 스크립트')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<section class="page-title">
<span class="badge text-bg-primary">형상 기준</span>
<h1 class="mt-2">보안 SQL 스크립트</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>ASO 마스킹, ORDS, Select AI 행 접근에 사용하는 Git 형상 기준 SQL을 읽기 전용으로 표시합니다. 이 화면은 SQL을 실행하거나 DB의 현재 source를 바꾸지 않습니다.</p>
</details>
</section>
<section class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}">조회할 수 없습니다.</section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>등록된 스크립트</h2>
<p class="section-subtitle">배포 JAR에는 아래 Git 추적 파일만 포함됩니다.</p>
</div>
<span class="badge text-bg-secondary" th:text="${#lists.size(scripts)}">0</span>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>구분</th><th>파일</th><th>용도</th><th></th></tr></thead>
<tbody>
<tr th:each="item : ${scripts}" th:classappend="${selectedScript != null and item.scriptId() == selectedScript.scriptId()} ? ' table-primary'">
<td><span class="badge text-bg-light" th:text="${item.category()}">ASO / 마스킹</span></td>
<td><code th:text="${item.fileName()}">62_kb_aso_masking_backoffice_metadata.sql</code></td>
<td>
<strong th:text="${item.title()}">컬럼 마스킹 규칙 메타데이터</strong>
<div class="form-hint" th:text="${item.description()}">설명</div>
</td>
<td><a class="btn btn-sm rw-btn-secondary" th:href="@{/security-sql-scripts(script=${item.scriptId()})}">원문 보기</a></td>
</tr>
<tr th:if="${#lists.isEmpty(scripts)}"><td colspan="4" class="text-muted">표시할 스크립트가 없습니다.</td></tr>
</tbody>
</table>
</div>
</section>
<section class="content-band" th:if="${selectedScript}">
<div class="section-heading">
<div>
<span class="badge text-bg-secondary" th:text="${selectedScript.category()}">ASO / 마스킹</span>
<h2 class="mt-2" th:text="${selectedScript.title()}">컬럼 마스킹 규칙 메타데이터</h2>
<p class="section-subtitle" th:text="${selectedScript.description()}">설명</p>
</div>
<code th:text="${selectedScript.fileName()}">62_kb_aso_masking_backoffice_metadata.sql</code>
</div>
<p class="form-hint">Git source: <code th:text="${'sql/adb/' + selectedScript.fileName()}">sql/adb/62_kb_aso_masking_backoffice_metadata.sql</code>. 실제 DB 배포본은 <a href="/vpd-filter-runtime">행 접근 필터 구조</a> 및 DB 배포 이력과 함께 확인하세요.</p>
<form hx-post="/security-sql-scripts/explanation" hx-target="#security-sql-explanation" hx-swap="innerHTML" class="mb-3">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="script" th:value="${selectedScript.scriptId()}">
<button class="btn rw-btn-primary" type="submit">LLM 전체·블록·토큰 처리 설명 생성</button>
<span class="form-hint ms-2">원문은 실행·수정하지 않고 설명을 만들 때만 AI에 전달합니다.</span>
</form>
<section id="security-sql-explanation" class="mb-3"></section>
<section class="probe-exchange"><pre th:text="${selectedScript.source()}">-- SQL source</pre></section>
</section>
</main>
</body>
</html>

View File

@@ -13,7 +13,7 @@
</div>
<div class="alert alert-warning">
이 화면은 관리자용 원장 미리보기입니다. 사용자별 VPD 적용 결과는 <a href="/probe">접근 검증</a>에서 확인하세요.
이 화면은 관리자용 원장 미리보기입니다. 사용자별 행 접근 적용 결과는 <a href="/probe">접근 검증</a>에서 확인하세요.
</div>
<section class="content-band">

View File

@@ -8,7 +8,7 @@
<h1>검증 세션</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>이해당사자 원장에 등록된 업무 사용자를 토큰에 연결합니다. 토큰 원문은 한 번만 표시하며, DB에는 해시와 식별용 prefix만 보관합니다. 역할·채널·행 권한은 토큰이 아니라 접근 규칙에서 동적으로 계산합니다.</p>
<p>이해당사자 원장에 등록된 업무 사용자를 토큰에 연결합니다. 토큰 원문은 한 번만 표시하며, DB에는 해시와 식별용 prefix만 보관합니다. 역할·채널·행 권한은 토큰이 아니라 접근 규칙에서 동적으로 계산합니다.</p>
</details>
</div>
@@ -28,7 +28,7 @@
<div class="section-heading">
<div>
<h2>이해당사자 토큰 발급</h2>
<p class="section-subtitle">모든 KB 이해당사자에게 토큰을 발급할 수 있습니다. 지점장·설계사 등의 실제 행 범위는 접근 규칙에 연결된 역할로 결정되므로, 토큰을 다시 만들지 않아도 권한 변경이 반영됩니다.</p>
<p class="section-subtitle">모든 KB 이해당사자에게 토큰을 발급할 수 있습니다. 지점장·설계사 등의 실제 행 범위는 접근 규칙에 연결된 역할로 결정되므로, 토큰을 다시 만들지 않아도 권한 변경이 반영됩니다.</p>
</div>
</div>
<form method="post" action="/tokens" class="form-grid">

View File

@@ -0,0 +1,109 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('컬럼 원문 표시 허용 사용자')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<div class="page-title">
<h1>컬럼 원문 표시 허용 사용자</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>이 화면은 <a href="/masking-rules">컬럼 마스킹</a>에서 연결한 기본 컬럼 마스킹 규칙에 대해 <strong>원문 표시 허용(UNMASK)</strong> 사용자만 관리합니다. 이 목록에 없는 사용자는 행 접근 정책이 조회를 허용하더라도 해당 컬럼의 마스킹값을 받습니다.</p>
<p>행 접근 여부는 변경하지 않습니다. 행 접근 권한이 없으면 행을 볼 수 없고, 행 접근 권한이 있으며 이 목록에 없으면 마스킹값을 봅니다. 행 접근 권한이 있고 이 목록에 있으면 그 허용 범위 안에서만 원문을 봅니다. 저장한 변경은 DBMS_REDACT가 읽는 세션 context에 반영됩니다.</p>
</details>
</div>
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
<section class="content-band">
<h2>컬럼 원문 표시 허용 사용자 추가</h2>
<p class="section-subtitle">먼저 컬럼 마스킹 화면에서 민감 컬럼에 활성 규칙을 연결하세요. 이 등록은 원문 표시만 허용하며 행 접근 권한은 부여하지 않습니다.</p>
<form method="post" action="/user-masking-rules" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
원문 표시를 허용할 사용자
<select class="form-select" name="userId" required>
<option value="">선택하세요</option>
<option th:each="user : ${users}" th:value="${user.userId()}" th:attr="data-user-label=${user.username()}"
th:text="${user.username() + ' (' + user.empNo() + ')'}">KB_VPD_ADMIN</option>
</select>
</label>
<label>
원문 표시 허용 대상 컬럼
<select class="form-select" name="columnId" required>
<option value="">선택하세요</option>
<option th:each="columnRule : ${columnRules}" th:value="${columnRule.columnId()}"
th:attr="data-target=${columnRule.targetLabel()},
data-template=${columnRule.template().label()},
data-result=${columnRule.template().previewResult()},
data-aso-function=${columnRule.template().asoFunction()},
data-context=${'MR_' + columnRule.columnId()}"
th:text="${columnRule.targetLabel() + ' · ' + columnRule.ruleLabel()}">KB_CUSTOMERS.RRN_MASKED</option>
</select>
</label>
<div>
<span class="form-label">적용 결과</span>
<div class="form-control bg-light" aria-label="적용 결과">행 접근 정책이 허용한 행에서만 원문 표시 · UNMASK</div>
</div>
<button class="btn btn-primary" type="submit">컬럼 원문 표시 허용 등록</button>
</form>
<div class="policy-preview mt-3" data-masking-exception-preview>
<h3>선택한 컬럼의 실제 적용 결과</h3>
<p class="section-description" data-masking-preview-empty>원문 표시 허용 대상 컬럼을 선택하면 일반 사용자와 허용 사용자의 반환값을 보여줍니다.</p>
<dl data-masking-preview-details hidden>
<div>
<dt>대상 · DB 마스킹 방식</dt>
<dd data-masking-preview-target>-</dd>
</div>
<div>
<dt>행 접근 권한이 없는 사용자</dt>
<dd>행 자체가 반환되지 않습니다.</dd>
</div>
<div>
<dt>행 접근 허용 · 원문 표시 허용 없음</dt>
<dd data-masking-preview-masked>-</dd>
</div>
<div class="policy-preview-emphasis">
<dt>행 접근 허용 · 원문 표시 허용 사용자</dt>
<dd data-masking-preview-unmasked>-</dd>
</div>
<div>
<dt>DB 판정 기준</dt>
<dd data-masking-preview-context>-</dd>
</div>
</dl>
</div>
</section>
<section class="content-band">
<h2>컬럼 원문 표시 허용 사용자 목록</h2>
<p class="section-subtitle">목록에 없는 사용자는 행 접근 정책이 허용한 행에서 기본 마스킹값을 봅니다.</p>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>사용자</th><th>대상 컬럼</th><th>기본 규칙</th><th>원문 표시 상태</th><th>상태</th><th></th></tr></thead>
<tbody>
<tr th:each="userRule : ${userRules}">
<td th:text="${userRule.username()}">KB_VPD_ADMIN</td>
<td><code th:text="${userRule.targetLabel()}">POC_2.KB_CUSTOMERS.RRN_MASKED</code></td>
<td th:text="${userRule.ruleLabel()}">주민번호 기본 마스킹 · 주민등록번호 부분 마스킹</td>
<td><span class="badge" th:classappend="${userRule.unmasked()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${userRule.decisionLabel()}">원문 표시 예외</span></td>
<td th:text="${userRule.activeYn()}">Y</td>
<td>
<form method="post" action="/user-masking-rules/delete" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="userId" th:value="${userRule.userId()}">
<input type="hidden" name="columnId" th:value="${userRule.columnId()}">
<button class="btn btn-sm btn-outline-danger" type="submit">원문 표시 허용 해제</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(userRules)}"><td colspan="6" class="text-muted">원문 표시 허용 사용자가 없습니다. 행 접근 정책이 허용한 사용자에게 기본 컬럼 마스킹 규칙이 적용됩니다.</td></tr>
</tbody>
</table>
</div>
</section>
</main>
</body>
</html>

View File

@@ -83,9 +83,9 @@
<div class="section-heading">
<div>
<h2>접근 정책 설정</h2>
<p class="section-subtitle">역할별 기술 태그 접근 규칙은 권한 관리에서 관리합니다.</p>
<p class="section-subtitle">역할별 기술 태그 접근 규칙은 행 접근 규칙 화면에서 관리합니다.</p>
</div>
<a class="btn btn-sm rw-btn-primary" href="/permissions">권한 관리 열기</a>
<a class="btn btn-sm rw-btn-primary" href="/permissions">행 접근 규칙 열기</a>
</div>
<div class="macro-micro-grid">
<div>
@@ -121,7 +121,7 @@ AND NOT (DENY TAG C OR DENY TAG D)</pre>
<ol>
<li>검색어를 같은 임베딩 방식으로 벡터화합니다.</li>
<li>임시 Bearer Token으로 전용 ORDS Handler를 호출합니다.</li>
<li>VPD가 TECH_TAG 권한에 맞지 않는 검색 단위를 먼저 제거합니다.</li>
<li>행 접근 정책이 TECH_TAG 권한에 맞지 않는 검색 단위를 먼저 제거합니다.</li>
<li>남은 검색 단위를 관련도 순으로 반환합니다.</li>
</ol>
</details>

View File

@@ -124,7 +124,7 @@
<td colspan="4" class="policy-source-cell">
<div th:id="${'filter-source-' + iter.index}" class="text-muted small">Source 보기를 누르면 현재 함수 내용을 표시합니다.</div>
<div class="alert alert-light mt-3 mb-0" th:if="${function.permissionSystemDefault()}">
이 함수는 권한체계의 핵심 실행 경로이므로 직접 수정할 수 없습니다. 권한 규칙을 변경하세요.
이 함수는 권한체계의 핵심 실행 경로이므로 직접 수정할 수 없습니다. 행 접근 규칙을 변경하세요.
</div>
<form method="post" action="/vpd-filter-policies/filters" class="filter-edit-form mt-3"
th:unless="${function.permissionSystemDefault()}">

View File

@@ -0,0 +1,126 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('행 접근 필터 구조')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<section class="page-title">
<span class="badge text-bg-primary">운영 기준</span>
<h1 class="mt-2">행 접근 필터 구조</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>기본 동적 필터 <code>CB_AGENT_DOC_VPD_FILTER</code>의 연결 상태와 현재 DB에 배포된 함수를 읽기 전용으로 보여줍니다. 행 접근 변경은 <a href="/permissions">행 접근 규칙</a>에서 수행합니다.</p>
<section class="content-band mt-3 mb-0">
<h2>행 필터(VPD) 적용 흐름</h2>
<p><strong>Oracle VPD 정책 함수는 Bearer Token을 직접 해석하지 않습니다.</strong> 앞 단계의 <code>cb_agent_ctx_pkg.set_user_by_bearer</code>가 만든 <code>CB_AGENT_CTX</code>를 읽어, 현재 SELECT에 붙일 WHERE predicate를 생성합니다.</p>
<div class="diagram-canvas">
<svg class="product-flow-diagram" viewBox="0 0 1120 410" role="img" aria-labelledby="vpd-help-flow-title vpd-help-flow-desc">
<title id="vpd-help-flow-title">Bearer 토큰 기반 행 필터 적용 흐름</title>
<desc id="vpd-help-flow-desc">토큰으로 설정된 사용자 컨텍스트를 행 접근 함수가 읽고 역할과 행 접근 규칙을 WHERE 조건으로 변환해, 허용된 행만 SELECT 결과에 남긴다.</desc>
<defs>
<marker id="flow-arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z"/></marker>
</defs>
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M185 92 H225"/>
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M405 92 H445"/>
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M625 92 H665"/>
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M845 92 H885"/>
<path class="diagram-link diagram-link-secondary" marker-end="url(#flow-arrow)" d="M745 212 V144"/>
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M977 136 V315"/>
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M900 136 V280 H755 V315"/>
<g class="diagram-node"><rect x="25" y="48" width="160" height="88" rx="12"/><text x="105" y="79" class="diagram-node-title">Bearer Token</text><text x="105" y="106" class="diagram-node-detail">ORDS 요청</text></g>
<g class="diagram-node"><rect x="225" y="48" width="180" height="88" rx="12"/><text x="315" y="76" class="diagram-node-title">토큰 검증·해석</text><text x="315" y="103" class="diagram-node-detail">set_user_by_bearer</text><text x="315" y="120" class="diagram-node-note">실패 시 context 정리</text></g>
<g class="diagram-node"><rect x="445" y="48" width="180" height="88" rx="12"/><text x="535" y="76" class="diagram-node-title">CB_AGENT_CTX</text><text x="535" y="103" class="diagram-node-detail">USER_ID · DEPT_CODE · EMP_NO</text><text x="535" y="120" class="diagram-node-note">STAKEHOLDER_USER_ID · CHANNEL</text></g>
<g class="diagram-node diagram-node-accent"><rect x="665" y="48" width="180" height="88" rx="12"/><text x="755" y="76" class="diagram-node-title">행 접근 함수 호출</text><text x="755" y="103" class="diagram-node-detail">CB_AGENT_DOC_VPD_FILTER</text><text x="755" y="120" class="diagram-node-note">p_object = 조회 대상</text></g>
<g class="diagram-node diagram-node-accent"><rect x="885" y="48" width="185" height="88" rx="12"/><text x="977" y="76" class="diagram-node-title">WHERE predicate</text><text x="977" y="103" class="diagram-node-detail">ALLOW / DENY 결합</text><text x="977" y="120" class="diagram-node-note">원래 SELECT에 자동 추가</text></g>
<g class="diagram-node diagram-node-data"><rect x="590" y="212" width="310" height="72" rx="12"/><text x="745" y="239" class="diagram-node-title">역할·권한·규칙 메타데이터</text><text x="745" y="263" class="diagram-node-detail">cb_user_role · cb_group_role · cb_permission_rule</text></g>
<g class="diagram-node"><rect x="665" y="315" width="180" height="64" rx="12"/><text x="755" y="341" class="diagram-node-title">ALLOW 없음 / USER_ID 없음</text><text x="755" y="363" class="diagram-node-detail">RETURN 1 = 0 · 행 없음</text></g>
<g class="diagram-node diagram-node-data"><rect x="885" y="315" width="185" height="64" rx="12"/><text x="977" y="341" class="diagram-node-title">ALLOW 조건 충족</text><text x="977" y="363" class="diagram-node-detail">조건에 맞는 행만 반환</text></g>
</svg>
</div>
<p class="form-hint mb-0"><code>rule_type</code>별 WHERE 조각은 권한 내부에서 AND, ALLOW 권한끼리는 OR, DENY 권한은 최종적으로 <code>AND NOT (...)</code>으로 결합됩니다. ASO는 같은 context의 <code>MR_&lt;column_id&gt;</code><em>컬럼 표시</em>를 정하고, 행 접근 필터는 <code>USER_ID</code>·<code>DEPT_CODE</code>·<code>EMP_NO</code>·<code>STAKEHOLDER_*</code><em>보이는 행</em>을 정합니다.</p>
</section>
</details>
</section>
<section class="alert alert-warning" th:if="${runtimeError}">
<strong th:text="${runtimeError.title()}">DB 연결 설정이 필요합니다.</strong>
<span th:text="${runtimeError.message()}">DB 연결을 확인할 수 없습니다.</span>
</section>
<section class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}">조회할 수 없습니다.</section>
<section class="content-band">
<h2>실행 흐름</h2>
<div class="macro-micro-grid">
<div>
<h3>1. 신뢰 컨텍스트 설정</h3>
<p>ORDS가 Bearer Token을 검증하고 <code>CB_AGENT_CTX</code>에 사용자·이해관계자·채널 정보를 설정합니다.</p>
</div>
<div>
<h3>2. 권한과 규칙 계산</h3>
<p>직접 역할과 활성 그룹 역할의 SELECT 권한을 읽습니다. 한 권한의 규칙은 AND, 여러 ALLOW 권한은 OR로 결합합니다.</p>
</div>
<div>
<h3>3. DB WHERE 절 적용</h3>
<p>Oracle VPD 정책이 반환 predicate를 원래 SELECT에 자동 적용합니다. 권한이 없으면 <code>1 = 0</code>으로 행 접근을 막는 것이 기준입니다.</p>
</div>
</div>
<p class="form-hint mt-3 mb-0">행 접근은 이 행 필터(VPD)가 담당합니다. 컬럼 마스킹과 원문 표시 허용 사용자는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction에서만 제어합니다.</p>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>현재 연결된 행 접근 정책(VPD)</h2>
<p class="section-subtitle">기본 Filter를 실제로 호출하는 보호 객체 정책입니다.</p>
</div>
<span class="badge text-bg-secondary" th:text="${#lists.size(policies)}">0</span>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr><th>Object</th><th>Policy</th><th>Statement</th><th>상태</th><th>Filter</th></tr>
</thead>
<tbody>
<tr th:each="policy : ${policies}">
<td><code th:text="${policy.objectDisplayName()}">POC_2.KB_CONTRACTS</code></td>
<td><code th:text="${policy.policyName()}">KB_KB_CONTRACTS_ROW_POLICY</code></td>
<td th:text="${policy.statementTypes()}">SELECT</td>
<td><span class="badge" th:classappend="${policy.enabled() == 'YES'} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${policy.enabled() == 'YES'} ? '적용됨' : '중지됨'">적용됨</span></td>
<td><code th:text="${policy.functionDisplayName()}">ADMIN.CB_AGENT_DOC_VPD_FILTER</code></td>
</tr>
<tr th:if="${#lists.isEmpty(policies)}">
<td colspan="5" class="text-muted">기본 행 접근 Filter가 연결된 보호 객체를 찾지 못했습니다.</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<h2>운영 점검 기준</h2>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>점검 항목</th><th>확인 기준</th><th>조치 위치</th></tr></thead>
<tbody>
<tr><td>토큰 신뢰 경계</td><td>Bearer Token은 ORDS에서 검증하고 요청 종료 시 컨텍스트를 정리합니다.</td><td>ORDS Handler / <code>CB_AGENT_CTX_PKG</code></td></tr>
<tr><td>권한 결합</td><td>권한 내부 규칙은 AND, ALLOW 권한은 OR, DENY 조건은 최종적으로 제외합니다.</td><td><a href="/permissions">행 접근 규칙</a></td></tr>
<tr><td>오류·미권한</td><td>유효한 컨텍스트나 ALLOW 권한이 없으면 행 접근은 차단돼야 합니다.</td><td><a href="/probe">접근 검증</a></td></tr>
<tr><td>컬럼 보호</td><td>행 필터(VPD)와 보험료·주민번호 등의 ASO/Data Redaction 마스킹을 분리해 확인합니다.</td><td><a href="/masking-rules">컬럼 마스킹</a></td></tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<h2>배포된 함수 소스</h2>
<p class="section-subtitle">현재 DB의 <code>ALL_SOURCE</code>에서 읽은 소스입니다. 형상 기준 SQL과 다르면 배포 이력을 확인하세요.</p>
<section class="probe-exchange" th:if="${source}">
<h3><span th:text="${source.owner() + '.' + source.objectName()}">ADMIN.CB_AGENT_DOC_VPD_FILTER</span> <span class="text-muted" th:text="${' / ' + source.objectType()}"> / FUNCTION</span></h3>
<pre th:if="${source.found()}" th:text="${source.source()}"></pre>
<pre th:unless="${source.found()}">ALL_SOURCE에서 조회 가능한 소스가 없습니다. DB 연결 계정의 source 조회 권한을 확인하세요.</pre>
</section>
<div th:unless="${source}" class="text-muted">기본 Filter source를 조회할 연결 정보를 찾지 못했습니다.</div>
</section>
</main>
</body>
</html>

View File

@@ -8,7 +8,8 @@
<h1>보호 상태</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>권한 화면에서 만든 사용자·그룹·역할·행·열 규칙을 Oracle VPD가 실제 TABLE/VIEW에 적용하도록 연결합니다. Policy는 어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지 정하고, 실제 허용 조건은 권한 규칙과 Filter가 계산합니다.</p>
<p>행 접근 규칙 화면에서 만든 사용자·그룹·역할·행 규칙을 Oracle VPD가 실제 TABLE/VIEW에 적용하도록 연결합니다. Policy는 어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지 정하고, 실제 허용 조건은 행 접근 규칙과 Filter가 계산합니다.</p>
<p>이 화면은 <strong>행 접근 정책</strong>만 확인합니다. 컬럼 원문/마스킹 적용 상태는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 상태에서 확인하세요.</p>
</details>
</div>
@@ -23,10 +24,10 @@
<div class="section-heading">
<div>
<h2>보호 대상</h2>
<p class="section-subtitle">VPD 적용과 검증 준비 상태를 확인합니다.</p>
<p class="section-subtitle">행 접근 정책 적용과 검증 준비 상태를 확인합니다.</p>
<details class="explanation-details">
<summary>객체 상태 항목 설명 보기</summary>
<p>VPD가 붙었는지, 권한 규칙과 검증 경로가 준비됐는지 한곳에서 확인합니다. 아래 기본 적용에서는 객체만 선택하면 됩니다.</p>
<p>Oracle VPD 정책이 붙었는지, 행 접근 규칙과 검증 경로가 준비됐는지 한곳에서 확인합니다. 아래 기본 적용에서는 객체만 선택하면 됩니다.</p>
</details>
</div>
<span class="badge text-bg-secondary" th:text="${#lists.size(vpdTargets)}">0</span>