329 lines
16 KiB
HTML
329 lines
16 KiB
HTML
<!doctype html>
|
|
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
|
<head th:replace="~{fragments/layout :: head('VPD 설정')}"></head>
|
|
<body>
|
|
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
|
<main class="container py-4">
|
|
<div class="page-title">
|
|
<h1>VPD 설정</h1>
|
|
<p>Oracle Database의 TABLE/VIEW에 VPD policy와 policy function/filter predicate를 적용합니다.</p>
|
|
</div>
|
|
|
|
<section th:replace="~{fragments/layout :: architectureStrip('vpd')}"></section>
|
|
|
|
<div class="alert alert-warning" th:if="${runtimeError}">
|
|
<strong th:text="${runtimeError.title()}">조회할 수 없습니다.</strong>
|
|
<span th:text="${runtimeError.message()}">message</span>
|
|
</div>
|
|
<div class="alert alert-success" th:if="${successMessage}" th:text="${successMessage}">등록되었습니다.</div>
|
|
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}">처리할 수 없습니다.</div>
|
|
|
|
<section class="content-band">
|
|
<div class="section-heading">
|
|
<h2>VPD 적용</h2>
|
|
<a class="btn btn-sm rw-btn-primary" href="/vpd-filter-policies">Filter Policy 관리</a>
|
|
</div>
|
|
<p class="text-muted mb-0">VPD는 개별 보호 객체 적용을 기본으로 하고, 같은 정책을 스키마 TABLE/VIEW에 확장할 때만 벌크 적용을 사용합니다.</p>
|
|
</section>
|
|
|
|
<section class="content-band">
|
|
<div class="section-heading">
|
|
<div>
|
|
<h2>VPD 적용 대상 TABLE/VIEW</h2>
|
|
<p class="section-subtitle">Oracle DB catalog 기준의 TABLE/VIEW 목록입니다. ORDS는 이 객체를 HTTP로 서빙하는 별도 레이어입니다.</p>
|
|
</div>
|
|
<span class="badge text-bg-secondary" th:text="${#lists.size(vpdTargets)}">0</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Object</th>
|
|
<th>Type</th>
|
|
<th>VPD 상태</th>
|
|
<th>Policy</th>
|
|
<th>백오피스 권한 테이블</th>
|
|
<th>ORDS Path</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr th:each="target : ${vpdTargets}">
|
|
<td><code th:text="${target.objectDisplayName()}">ADMIN.TABLE</code></td>
|
|
<td th:text="${target.objectType()}">TABLE</td>
|
|
<td>
|
|
<span class="badge"
|
|
th:classappend="${target.vpdApplied()} ? ' text-bg-success' : ' text-bg-secondary'"
|
|
th:text="${target.vpdApplied()} ? 'VPD 적용됨' : '미적용'">미적용</span>
|
|
</td>
|
|
<td>
|
|
<span th:if="${target.vpdApplied()}" th:text="${target.policyNames()}">POLICY</span>
|
|
<span th:unless="${target.vpdApplied()}" class="text-muted">-</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge"
|
|
th:classappend="${target.protectedObject()} ? ' text-bg-primary' : ' text-bg-light'"
|
|
th:text="${target.protectedObject()} ? '등록됨' : '미등록'">미등록</span>
|
|
</td>
|
|
<td>
|
|
<code th:if="${target.ordsPath()}" th:text="${target.ordsPath()}">cb-ords/path</code>
|
|
<span th:unless="${target.ordsPath()}" class="text-muted">ORDS 서빙 미등록</span>
|
|
</td>
|
|
</tr>
|
|
<tr th:if="${#lists.isEmpty(vpdTargets)}">
|
|
<td colspan="6" class="text-muted">조회 가능한 TABLE/VIEW가 없습니다. DB 연결 사용자 권한과 스키마 객체를 확인하세요.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="content-band">
|
|
<div class="section-heading">
|
|
<h2>개별 적용</h2>
|
|
</div>
|
|
<form method="post" action="/vpd-policies" class="form-grid">
|
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
|
<label>
|
|
VPD 적용 대상 TABLE/VIEW
|
|
<select class="form-select" name="objectKey" required>
|
|
<option th:each="target : ${vpdTargets}"
|
|
th:value="${target.objectDisplayName()}"
|
|
th:text="${target.objectDisplayName() + ' / ' + target.objectType() + (target.vpdApplied() ? ' / VPD 적용됨' : ' / 미적용')}"></option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Policy 이름
|
|
<input class="form-control" name="policyName" list="vpd-policy-names"
|
|
placeholder="예: CB_AGENT_DOC_POLICY" required>
|
|
</label>
|
|
<label class="span-2">
|
|
VPD Filter Function
|
|
<select class="form-select" name="functionKey">
|
|
<option value="">기본 권한 함수가 없으면 Filter predicate로 새 function 자동 생성</option>
|
|
<option th:each="function : ${formOptions.functions()}"
|
|
th:value="${function.value()}"
|
|
th:text="${function.functionName() == 'CB_AGENT_DOC_VPD_FILTER' ? function.label() + ' / 기본 권한 테이블 필터' : function.label()}"
|
|
th:selected="${function.value() == formOptions.defaultPermissionFunctionKey()}"></option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Function Owner
|
|
<select class="form-select" name="functionOwner">
|
|
<option value="">현재 연결 사용자</option>
|
|
<option th:each="owner : ${formOptions.owners()}"
|
|
th:value="${owner}"
|
|
th:text="${owner}"></option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Function 이름
|
|
<input class="form-control" name="functionName" list="vpd-function-names"
|
|
placeholder="POLICY_NAME_FILTER 자동 생성">
|
|
</label>
|
|
<div>
|
|
Statement Types
|
|
<div class="checkbox-row">
|
|
<label class="form-check" th:each="statement : ${formOptions.statementTypes()}">
|
|
<input class="form-check-input" type="checkbox" name="statementTypes"
|
|
th:value="${statement}" th:checked="${statement == 'SELECT'}">
|
|
<span class="form-check-label" th:text="${statement}">SELECT</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-check align-self-end">
|
|
<input class="form-check-input" id="vpd-enabled" type="checkbox" name="enabled" value="true" checked>
|
|
<label class="form-check-label" for="vpd-enabled">등록 즉시 활성화</label>
|
|
</div>
|
|
<div class="form-check span-2">
|
|
<input class="form-check-input" id="vpd-update-check" type="checkbox" name="updateCheck" value="true">
|
|
<label class="form-check-label" for="vpd-update-check">INSERT/UPDATE에도 predicate check 적용</label>
|
|
</div>
|
|
<label class="span-2">
|
|
Filter predicate
|
|
<textarea class="form-control" id="vpd-filter-predicate" name="filterPredicate" rows="4"
|
|
placeholder="예: dept_code = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'') 비우면 기존 Function 이름으로 ADD_POLICY만 실행합니다."></textarea>
|
|
</label>
|
|
<div class="question-presets span-2" aria-label="Filter predicate 예시">
|
|
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="1=0">전체 차단</button>
|
|
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="1=1">전체 허용</button>
|
|
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="dept_code = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE')">부서 일치</button>
|
|
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="owner_emp_no = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')">본인 소유</button>
|
|
</div>
|
|
<button class="btn rw-btn-primary" type="submit">개별 객체에 VPD 적용</button>
|
|
</form>
|
|
<datalist id="vpd-policy-names">
|
|
<option th:each="policyName : ${formOptions.policyNames()}" th:value="${policyName}"></option>
|
|
</datalist>
|
|
<datalist id="vpd-function-names">
|
|
<option th:each="function : ${formOptions.functions()}" th:value="${function.functionName()}"></option>
|
|
</datalist>
|
|
</section>
|
|
|
|
<section class="content-band">
|
|
<details>
|
|
<summary class="bulk-apply-summary">벌크 적용</summary>
|
|
<form method="post" action="/vpd-policies/bulk" class="form-grid mt-3">
|
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
|
<label>
|
|
Schema
|
|
<select class="form-select" name="schemaOwner" required>
|
|
<option th:each="owner : ${formOptions.schemaOwners()}" th:value="${owner}" th:text="${owner}"></option>
|
|
</select>
|
|
</label>
|
|
<div>
|
|
Object Types
|
|
<div class="checkbox-row">
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="includeTables" value="true" checked>
|
|
<span class="form-check-label">TABLE</span>
|
|
</label>
|
|
<label class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="includeViews" value="true" checked>
|
|
<span class="form-check-label">VIEW</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<label class="span-2">
|
|
VPD Filter Function
|
|
<select class="form-select" name="functionKey">
|
|
<option value="">기본 권한 함수가 없으면 Filter predicate로 벌크 function 자동 생성</option>
|
|
<option th:each="function : ${formOptions.functions()}"
|
|
th:value="${function.value()}"
|
|
th:text="${function.functionName() == 'CB_AGENT_DOC_VPD_FILTER' ? function.label() + ' / 기본 권한 테이블 필터' : function.label()}"
|
|
th:selected="${function.value() == formOptions.defaultPermissionFunctionKey()}"></option>
|
|
</select>
|
|
</label>
|
|
<div>
|
|
Statement Types
|
|
<div class="checkbox-row">
|
|
<label class="form-check" th:each="statement : ${formOptions.statementTypes()}">
|
|
<input class="form-check-input" type="checkbox" name="statementTypes"
|
|
th:value="${statement}" th:checked="${statement == 'SELECT'}">
|
|
<span class="form-check-label" th:text="${statement}">SELECT</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-check align-self-end">
|
|
<input class="form-check-input" id="bulk-vpd-enabled" type="checkbox" name="enabled" value="true" checked>
|
|
<label class="form-check-label" for="bulk-vpd-enabled">등록 즉시 활성화</label>
|
|
</div>
|
|
<div class="form-check span-2">
|
|
<input class="form-check-input" id="bulk-vpd-update-check" type="checkbox" name="updateCheck" value="true">
|
|
<label class="form-check-label" for="bulk-vpd-update-check">INSERT/UPDATE에도 predicate check 적용</label>
|
|
</div>
|
|
<label class="span-2">
|
|
Filter predicate
|
|
<textarea class="form-control" id="bulk-vpd-filter-predicate" name="filterPredicate" rows="4"
|
|
placeholder="기존 Function을 선택하지 않으면 이 predicate로 스키마 공통 function을 생성합니다."></textarea>
|
|
</label>
|
|
<button class="btn rw-btn-primary" type="submit">스키마 TABLE/VIEW에 VPD 일괄 적용</button>
|
|
</form>
|
|
</details>
|
|
</section>
|
|
|
|
<section class="content-band">
|
|
<div class="section-heading">
|
|
<h2>VPD Policies</h2>
|
|
<span class="badge text-bg-secondary" th:text="${#lists.size(policies)}">0</span>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Object</th>
|
|
<th>Policy</th>
|
|
<th>Function</th>
|
|
<th>Statements</th>
|
|
<th>Status</th>
|
|
<th>Type</th>
|
|
<th>Options</th>
|
|
<th>Explain</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<th:block th:each="policy, iter : ${policies}">
|
|
<tr>
|
|
<td>
|
|
<strong th:text="${policy.objectDisplayName()}">ADMIN.TABLE</strong>
|
|
<div class="text-muted small" th:text="${policy.policyGroup()}">SYS_DEFAULT</div>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-secondary policy-function-button"
|
|
type="button"
|
|
th:hx-get="@{/vpd-policies/policy-detail(objectOwner=${policy.objectOwner()},objectName=${policy.objectName()},policyName=${policy.policyName()})}"
|
|
th:hx-target="${'#policy-source-' + iter.index}"
|
|
hx-swap="innerHTML">
|
|
<code th:text="${policy.policyName()}">POLICY</code>
|
|
</button>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-secondary policy-function-button"
|
|
type="button"
|
|
th:hx-get="@{/vpd-policies/function-source(owner=${policy.functionOwner()},packageName=${policy.packageName()},functionName=${policy.functionName()})}"
|
|
th:hx-target="${'#policy-source-' + iter.index}"
|
|
hx-swap="innerHTML">
|
|
<code th:text="${policy.functionDisplayName()}">OWNER.FUNC</code>
|
|
</button>
|
|
</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 th:text="${policy.policyType()}">DYNAMIC</td>
|
|
<td>
|
|
<span>Check: <strong th:text="${policy.checkOption()}">NO</strong></span>
|
|
<span class="ms-2">Static: <strong th:text="${policy.staticPolicy()}">NO</strong></span>
|
|
<span class="ms-2">Long: <strong th:text="${policy.longPredicate()}">NO</strong></span>
|
|
</td>
|
|
<td>
|
|
<div class="action-stack">
|
|
<button class="btn btn-sm rw-btn-primary"
|
|
type="button"
|
|
th:hx-get="@{/vpd-policies/policy-explanation(objectOwner=${policy.objectOwner()},objectName=${policy.objectName()},policyName=${policy.policyName()})}"
|
|
th:hx-target="${'#policy-source-' + iter.index}"
|
|
th:hx-indicator="${'#policy-loading-' + iter.index}"
|
|
hx-swap="innerHTML">
|
|
LLM 설명
|
|
</button>
|
|
<span th:id="${'policy-loading-' + iter.index}" class="htmx-indicator loading-pill">
|
|
<span class="loading-spinner" aria-hidden="true"></span>
|
|
분석 중
|
|
</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="8" class="policy-source-cell">
|
|
<div th:id="${'policy-source-' + iter.index}" class="text-muted small">
|
|
Policy, policy function, LLM 설명을 클릭하면 상세 내역이 표시됩니다.
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</th:block>
|
|
<tr th:if="${#lists.isEmpty(policies)}">
|
|
<td colspan="8" class="text-muted">
|
|
등록된 TABLE/VIEW에 적용된 VPD policy가 없습니다. 보호 객체 등록 상태와 DB policy 적용 상태를 확인하세요.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<script>
|
|
document.querySelectorAll('.question-preset').forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
const target = document.getElementById(button.dataset.target || '');
|
|
if (target) {
|
|
target.value = button.dataset.question || '';
|
|
target.focus();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|