fix #557: guide permission-driven VPD flow

This commit is contained in:
devmrko
2026-06-29 12:11:25 +09:00
parent fbe3d4682b
commit 908ac9a386
57 changed files with 1952 additions and 312 deletions

View File

@@ -5,8 +5,15 @@
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<div class="page-title">
<h1>ORDS 조회 Handler 생성</h1>
<p>VPD가 적용된 TABLE/VIEW를 ORDS HTTP 경로로 서빙하기 위한 조회 Handler를 등록합니다.</p>
<h1>ORDS 조회 대상 등록</h1>
<p>이 화면은 DB 객체를 HTTP로 연결할 경로만 등록합니다. 누가 어떤 행을 볼 수 있는지는 권한 규칙과 VPD에서 결정합니다.</p>
</div>
<div class="alert alert-info">
<strong>벡터 지식자료 검색 흐름:</strong>
문서를 청크로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다.
그 다음 <a href="/permissions">권한 관리</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면
태그 중 하나라도 맞는 청크만 검색됩니다.
</div>
<section th:replace="~{fragments/layout :: architectureStrip('ords')}"></section>
@@ -15,7 +22,8 @@
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
<section class="content-band">
<h2>조회 Handler 대상 추가</h2>
<h2>조회 대상 추가 (예제 Handler)</h2>
<p class="section-subtitle">DB 객체 하나를 하나의 조회 Handler로 연결하는 예제입니다. 대상 추가 후 Handler를 생성하면 VPD context 설정과 기본 SELECT가 포함된 PL/SQL이 만들어지며, 소스 보기에서 실제 업무에 맞게 수정할 수 있습니다.</p>
<form method="post" action="/objects" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="owner" required>
@@ -32,15 +40,40 @@
</select>
</label>
<label>
ORDS Path
<input class="form-control" name="ordsPath" placeholder="DB 객체 선택 시 자동 입력" required>
ORDS Path (자동)
<input class="form-control" name="ordsPath" data-ords-path-input
placeholder="DB 객체 선택 시 자동 입력" readonly required>
<span class="form-hint">
객체를 선택하면 예제용 module/template 경로가 자동으로 입력됩니다. 비워 제출해도 서버가 같은 규칙으로 채웁니다.
<label class="form-check form-check-inline ms-2">
<input class="form-check-input" type="checkbox" data-ords-path-edit>
<span class="form-check-label">고급: 직접 수정</span>
</label>
</span>
</label>
<label>
조회 대상 설명
<input class="form-control" name="description" maxlength="200"
placeholder="예: 기술 태그 권한이 적용된 지식자료 검색">
</label>
<button class="btn btn-primary" type="submit">대상 추가</button>
</form>
<div class="alert alert-info mt-3 mb-0">
<strong>생성되는 예제 흐름:</strong>
<code>cb_ords_handler_pkg.set_vpd_context(:auth_header)</code>로 토큰의 사용자·역할 컨텍스트를 넣고,
선택한 한 테이블에 <code>SELECT ... FROM OWNER.TABLE</code>을 실행한 뒤 JSON으로 반환합니다.
이것은 유일한 사용 방식이 아니라 시작점이며, Handler 소스와 ORDS 메타데이터에서 수정할 수 있습니다.
컬럼 민감도·마스킹은 이 화면에서 다루지 않고 <a href="/permissions">권한 관리의 원문 표시 허용 컬럼</a>에서 별도로 설정합니다.
</div>
</section>
<section class="content-band">
<h2>조회 Handler 대상 목록</h2>
<div class="section-heading">
<div>
<h2>조회 대상 목록</h2>
<p class="section-subtitle">ORDS Path와 Handler 상태를 관리합니다. 접근 권한은 이 표에서 만들지 않습니다.</p>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
@@ -48,6 +81,7 @@
<th>ID</th>
<th>Owner</th>
<th>Object</th>
<th>설명</th>
<th>ORDS Path</th>
<th>Action</th>
</tr>
@@ -58,6 +92,16 @@
<td th:text="${object.objectId()}">1</td>
<td th:text="${object.owner()}">ADMIN</td>
<td th:text="${object.objectName()}">CB_V_SEARCH_DOCUMENTS</td>
<td>
<form method="post" action="/objects/description" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="objectId" th:value="${object.objectId()}">
<input class="form-control form-control-sm" name="description" maxlength="200"
th:value="${object.description()}" placeholder="짧은 설명">
<button class="btn btn-sm btn-outline-primary" type="submit">저장</button>
</form>
<small class="text-muted" th:text="${object.descriptionOrDefault()}">조회 대상 설명</small>
</td>
<td>
<form method="post" action="/objects/ords-path" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
@@ -67,7 +111,10 @@
</form>
</td>
<td>
<div class="action-stack">
<div class="action-stack" th:if="${object.objectName() == 'CB_VECTOR_SEARCH_DOCUMENTS'}">
<span class="text-info small">전용 벡터 Handler 사용<br>(SQL 29, EMBEDDING 미노출)</span>
</div>
<div class="action-stack" th:unless="${object.objectName() == 'CB_VECTOR_SEARCH_DOCUMENTS'}">
<form method="post" action="/objects/ords-handler" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="objectId" th:value="${object.objectId()}">
@@ -87,74 +134,24 @@
</td>
</tr>
<tr>
<td colspan="5" class="policy-source-cell">
<details class="column-policy-panel">
<summary>기본 컬럼 민감도/마스킹 정책</summary>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Column</th>
<th>민감도</th>
<th>마스킹</th>
<th>상태</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="column : ${columnsByObject[object.objectId()]}">
<td>
<span th:text="${column.columnName()}">CONTENTS</span>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"
th:form="${'column-policy-form-' + column.columnId()}">
<input type="hidden" name="columnId" th:value="${column.columnId()}"
th:form="${'column-policy-form-' + column.columnId()}">
</td>
<td>
<select class="form-select form-select-sm" name="sensitivityLevel"
th:form="${'column-policy-form-' + column.columnId()}">
<option value="PUBLIC" th:selected="${column.sensitivityLevel() == 'PUBLIC'}">PUBLIC</option>
<option value="INTERNAL" th:selected="${column.sensitivityLevel() == 'INTERNAL'}">INTERNAL</option>
<option value="CONFIDENTIAL" th:selected="${column.sensitivityLevel() == 'CONFIDENTIAL'}">CONFIDENTIAL</option>
<option value="RESTRICTED" th:selected="${column.sensitivityLevel() == 'RESTRICTED'}">RESTRICTED</option>
</select>
</td>
<td>
<select class="form-select form-select-sm" name="redactionMethod"
th:form="${'column-policy-form-' + column.columnId()}">
<option value="NONE" th:selected="${column.redactionMethod() == 'NONE'}">NONE</option>
<option value="NULLIFY" th:selected="${column.redactionMethod() == 'NULLIFY'}">NULLIFY</option>
<option value="PARTIAL" th:selected="${column.redactionMethod() == 'PARTIAL'}">PARTIAL</option>
<option value="FULL" th:selected="${column.redactionMethod() == 'FULL'}">FULL</option>
</select>
</td>
<td>
<span class="badge"
th:classappend="${column.sensitive()} ? ' text-bg-warning' : ' text-bg-secondary'"
th:text="${column.policyLabel()}">PUBLIC/NONE</span>
</td>
<td>
<form method="post" action="/objects/column-policy"
th:id="${'column-policy-form-' + column.columnId()}"></form>
<button class="btn btn-sm btn-outline-primary" type="submit"
th:form="${'column-policy-form-' + column.columnId()}">저장</button>
</td>
</tr>
<tr th:if="${#lists.isEmpty(columnsByObject[object.objectId()])}">
<td colspan="5" class="text-muted">등록된 컬럼 정책이 없습니다.</td>
</tr>
</tbody>
</table>
</div>
</details>
<div th:id="${'handler-source-' + object.objectId()}" class="text-muted small">
<td colspan="6" class="policy-source-cell">
<div class="object-handler-explainer">
<strong>이 대상에서 자동으로 연결되는 것</strong>
<span>토큰 헤더 → VPD context 설정 → 선택한 단일 테이블의 기본 SELECT → JSON 응답</span>
<small>행 접근은 권한 규칙/VPD가 담당합니다. 원문 표시 예외는 권한 관리 Step 4에서 여러 컬럼을 등록하세요.</small>
</div>
<div th:if="${object.objectName() != 'CB_VECTOR_SEARCH_DOCUMENTS'}"
th:id="${'handler-source-' + object.objectId()}" class="text-muted small">
소스 보기를 누르면 기본 Handler PL/SQL이 표시됩니다.
</div>
<div th:if="${object.objectName() == 'CB_VECTOR_SEARCH_DOCUMENTS'}" class="text-muted small">
전용 벡터 검색 Handler는 SQL 29에서 등록합니다.
</div>
</td>
</tr>
</th:block>
<tr th:if="${#lists.isEmpty(objects)}">
<td colspan="5" class="text-muted">등록된 조회 Handler 대상이 없습니다.</td>
<td colspan="6" class="text-muted">등록된 조회 Handler 대상이 없습니다.</td>
</tr>
</tbody>
</table>