fix #424: add filter policy management tab
This commit is contained in:
@@ -26,6 +26,17 @@ public class VpdPolicyController {
|
|||||||
|
|
||||||
@GetMapping("/vpd-policies")
|
@GetMapping("/vpd-policies")
|
||||||
public String policies(Model model) {
|
public String policies(Model model) {
|
||||||
|
populatePolicyModel(model);
|
||||||
|
return "vpd-policies";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/vpd-filter-policies")
|
||||||
|
public String filterPolicies(Model model) {
|
||||||
|
populatePolicyModel(model);
|
||||||
|
return "vpd-filter-policies";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populatePolicyModel(Model model) {
|
||||||
try {
|
try {
|
||||||
model.addAttribute("policies", vpdPolicyService.findPolicies());
|
model.addAttribute("policies", vpdPolicyService.findPolicies());
|
||||||
model.addAttribute("objects", protectedObjectService.findEnabled());
|
model.addAttribute("objects", protectedObjectService.findEnabled());
|
||||||
@@ -37,7 +48,6 @@ public class VpdPolicyController {
|
|||||||
model.addAttribute("objects", List.of());
|
model.addAttribute("objects", List.of());
|
||||||
model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
|
model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
|
||||||
}
|
}
|
||||||
return "vpd-policies";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/vpd-policies")
|
@PostMapping("/vpd-policies")
|
||||||
@@ -52,6 +62,41 @@ public class VpdPolicyController {
|
|||||||
@RequestParam(defaultValue = "false") boolean updateCheck,
|
@RequestParam(defaultValue = "false") boolean updateCheck,
|
||||||
@RequestParam(required = false) String filterPredicate,
|
@RequestParam(required = false) String filterPredicate,
|
||||||
RedirectAttributes redirectAttributes
|
RedirectAttributes redirectAttributes
|
||||||
|
) {
|
||||||
|
createPolicyInternal(objectKey, policyName, functionKey, functionOwner, functionName, statementTypes, enabled,
|
||||||
|
updateCheck, filterPredicate, redirectAttributes);
|
||||||
|
return "redirect:/vpd-policies";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/vpd-filter-policies")
|
||||||
|
public String createFilterPolicy(
|
||||||
|
@RequestParam String objectKey,
|
||||||
|
@RequestParam String policyName,
|
||||||
|
@RequestParam(required = false) String functionKey,
|
||||||
|
@RequestParam(required = false) String functionOwner,
|
||||||
|
@RequestParam(required = false) String functionName,
|
||||||
|
@RequestParam(defaultValue = "SELECT") List<String> statementTypes,
|
||||||
|
@RequestParam(defaultValue = "false") boolean enabled,
|
||||||
|
@RequestParam(defaultValue = "false") boolean updateCheck,
|
||||||
|
@RequestParam(required = false) String filterPredicate,
|
||||||
|
RedirectAttributes redirectAttributes
|
||||||
|
) {
|
||||||
|
createPolicyInternal(objectKey, policyName, functionKey, functionOwner, functionName, statementTypes, enabled,
|
||||||
|
updateCheck, filterPredicate, redirectAttributes);
|
||||||
|
return "redirect:/vpd-filter-policies";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPolicyInternal(
|
||||||
|
String objectKey,
|
||||||
|
String policyName,
|
||||||
|
String functionKey,
|
||||||
|
String functionOwner,
|
||||||
|
String functionName,
|
||||||
|
List<String> statementTypes,
|
||||||
|
boolean enabled,
|
||||||
|
boolean updateCheck,
|
||||||
|
String filterPredicate,
|
||||||
|
RedirectAttributes redirectAttributes
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
String[] objectParts = objectKey.split("\\.", 2);
|
String[] objectParts = objectKey.split("\\.", 2);
|
||||||
@@ -77,7 +122,6 @@ public class VpdPolicyController {
|
|||||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||||
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
||||||
}
|
}
|
||||||
return "redirect:/vpd-policies";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/vpd-policies/bulk")
|
@PostMapping("/vpd-policies/bulk")
|
||||||
@@ -93,6 +137,43 @@ public class VpdPolicyController {
|
|||||||
@RequestParam(defaultValue = "false") boolean updateCheck,
|
@RequestParam(defaultValue = "false") boolean updateCheck,
|
||||||
@RequestParam(required = false) String filterPredicate,
|
@RequestParam(required = false) String filterPredicate,
|
||||||
RedirectAttributes redirectAttributes
|
RedirectAttributes redirectAttributes
|
||||||
|
) {
|
||||||
|
bulkApplyPolicyInternal(schemaOwner, includeTables, includeViews, functionKey, functionOwner, functionName,
|
||||||
|
statementTypes, enabled, updateCheck, filterPredicate, redirectAttributes);
|
||||||
|
return "redirect:/vpd-policies";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/vpd-filter-policies/bulk")
|
||||||
|
public String bulkApplyFilterPolicy(
|
||||||
|
@RequestParam String schemaOwner,
|
||||||
|
@RequestParam(defaultValue = "false") boolean includeTables,
|
||||||
|
@RequestParam(defaultValue = "false") boolean includeViews,
|
||||||
|
@RequestParam(required = false) String functionKey,
|
||||||
|
@RequestParam(required = false) String functionOwner,
|
||||||
|
@RequestParam(required = false) String functionName,
|
||||||
|
@RequestParam(defaultValue = "SELECT") List<String> statementTypes,
|
||||||
|
@RequestParam(defaultValue = "false") boolean enabled,
|
||||||
|
@RequestParam(defaultValue = "false") boolean updateCheck,
|
||||||
|
@RequestParam(required = false) String filterPredicate,
|
||||||
|
RedirectAttributes redirectAttributes
|
||||||
|
) {
|
||||||
|
bulkApplyPolicyInternal(schemaOwner, includeTables, includeViews, functionKey, functionOwner, functionName,
|
||||||
|
statementTypes, enabled, updateCheck, filterPredicate, redirectAttributes);
|
||||||
|
return "redirect:/vpd-filter-policies";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bulkApplyPolicyInternal(
|
||||||
|
String schemaOwner,
|
||||||
|
boolean includeTables,
|
||||||
|
boolean includeViews,
|
||||||
|
String functionKey,
|
||||||
|
String functionOwner,
|
||||||
|
String functionName,
|
||||||
|
List<String> statementTypes,
|
||||||
|
boolean enabled,
|
||||||
|
boolean updateCheck,
|
||||||
|
String filterPredicate,
|
||||||
|
RedirectAttributes redirectAttributes
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
var result = vpdPolicyService.bulkApplySchema(
|
var result = vpdPolicyService.bulkApplySchema(
|
||||||
@@ -114,7 +195,6 @@ public class VpdPolicyController {
|
|||||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||||
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
||||||
}
|
}
|
||||||
return "redirect:/vpd-policies";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/vpd-policies/function-source")
|
@GetMapping("/vpd-policies/function-source")
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<button class="rw-menu-trigger" type="button">운영</button>
|
<button class="rw-menu-trigger" type="button">운영</button>
|
||||||
<div class="rw-menu-panel">
|
<div class="rw-menu-panel">
|
||||||
<a class="nav-link" href="/vpd-policies">VPD 설정</a>
|
<a class="nav-link" href="/vpd-policies">VPD 설정</a>
|
||||||
|
<a class="nav-link" href="/vpd-filter-policies">Filter Policy 관리</a>
|
||||||
<a class="nav-link" href="/settings">설정</a>
|
<a class="nav-link" href="/settings">설정</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
163
src/main/resources/templates/vpd-filter-policies.html
Normal file
163
src/main/resources/templates/vpd-filter-policies.html
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head th:replace="~{fragments/layout :: head('Filter Policy 관리')}"></head>
|
||||||
|
<body>
|
||||||
|
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||||
|
<main class="container py-4">
|
||||||
|
<div class="page-title">
|
||||||
|
<h1>Filter Policy 관리</h1>
|
||||||
|
<p>VPD filter predicate와 policy function을 등록하고, 단건 또는 스키마 단위로 DBMS_RLS policy를 적용합니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>Filter Policy 등록</h2>
|
||||||
|
<a class="btn btn-sm rw-btn-secondary" href="/vpd-policies">적용 현황 보기</a>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/vpd-filter-policies" class="form-grid">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<label>
|
||||||
|
VPD 적용 대상
|
||||||
|
<select class="form-select" name="objectKey" required>
|
||||||
|
<option th:each="object : ${objects}"
|
||||||
|
th:value="${object.owner() + '.' + object.objectName()}"
|
||||||
|
th:text="${object.displayName() + ' / ' + object.ordsPath()}"></option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Policy 이름
|
||||||
|
<select class="form-select" name="policyName" required>
|
||||||
|
<option th:each="policyName : ${formOptions.policyNames()}"
|
||||||
|
th:value="${policyName}"
|
||||||
|
th:text="${policyName}"></option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="span-2">
|
||||||
|
기존 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.label()}"></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 이름
|
||||||
|
<select class="form-select" name="functionName">
|
||||||
|
<option value="">POLICY_NAME_FILTER 자동 생성</option>
|
||||||
|
<option th:each="function : ${formOptions.functions()}"
|
||||||
|
th:value="${function.functionName()}"
|
||||||
|
th:text="${function.functionName()}"></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="filter-enabled" type="checkbox" name="enabled" value="true" checked>
|
||||||
|
<label class="form-check-label" for="filter-enabled">등록 즉시 활성화</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check span-2">
|
||||||
|
<input class="form-check-input" id="filter-update-check" type="checkbox" name="updateCheck" value="true">
|
||||||
|
<label class="form-check-label" for="filter-update-check">INSERT/UPDATE에도 predicate check 적용</label>
|
||||||
|
</div>
|
||||||
|
<label class="span-2">
|
||||||
|
Filter predicate
|
||||||
|
<textarea class="form-control" id="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="filter-predicate" data-question="1=0">전체 차단</button>
|
||||||
|
<button class="btn rw-btn-secondary question-preset" type="button" data-target="filter-predicate" data-question="1=1">전체 허용</button>
|
||||||
|
<button class="btn rw-btn-secondary question-preset" type="button" data-target="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="filter-predicate" data-question="owner_emp_no = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')">본인 소유</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn rw-btn-primary" type="submit">Filter Policy 등록</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="content-band">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h2>스키마 벌크 적용</h2>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/vpd-filter-policies/bulk" class="form-grid">
|
||||||
|
<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">
|
||||||
|
기존 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.label()}"></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-filter-enabled" type="checkbox" name="enabled" value="true" checked>
|
||||||
|
<label class="form-check-label" for="bulk-filter-enabled">등록 즉시 활성화</label>
|
||||||
|
</div>
|
||||||
|
<label class="span-2">
|
||||||
|
Filter predicate
|
||||||
|
<textarea class="form-control" id="bulk-filter-predicate" name="filterPredicate" rows="4"
|
||||||
|
placeholder="기존 Function을 선택하지 않으면 이 predicate로 스키마 공통 function을 생성합니다."></textarea>
|
||||||
|
</label>
|
||||||
|
<button class="btn rw-btn-primary" type="submit">스키마 전체 적용</button>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
Reference in New Issue
Block a user