fix #467: move masking grants into permission wizard
This commit is contained in:
@@ -26,10 +26,20 @@
|
||||
|
||||
## UI 반영
|
||||
|
||||
- ORDS 조회 Handler 대상 화면에서 객체별 컬럼 정책을 조회/수정한다.
|
||||
- ORDS 조회 Handler 대상 화면에서 객체별 기본 컬럼 정책을 조회/수정한다.
|
||||
- 역할 화면에서 `max_sensitivity_level`을 조회/수정한다.
|
||||
- 권한 관리 wizard에서 역할이 해당 보호 객체를 조회할 때 원문 표시를 허용할 마스킹 컬럼을 선택한다.
|
||||
- ORDS 검증 결과의 masked 컬럼은 `contents [CONFIDENTIAL/NULLIFY]`처럼 정책 라벨을 같이 표시한다.
|
||||
|
||||
## 권한 부여 시 컬럼 마스킹 처리
|
||||
|
||||
컬럼 마스킹의 기준 정보는 보호 객체 컬럼에 둔다. 하지만 실제 원문 표시 허용은 역할과 보호 객체를 묶는 권한에 붙인다. 따라서 운영자는 권한을 줄 때 다음 순서로 판단한다.
|
||||
|
||||
1. 보호 객체의 기본 컬럼 민감도/마스킹 정책을 확인한다.
|
||||
2. 역할의 민감도 허용 상한을 확인한다.
|
||||
3. 권한 wizard에서 해당 역할에 원문 표시를 허용할 컬럼만 선택한다.
|
||||
4. 선택하지 않은 마스킹 대상 컬럼은 기본 정책대로 NULL 처리 또는 마스킹된다.
|
||||
|
||||
## 남은 판단
|
||||
|
||||
`cb_permission_column`과 `max_sensitivity_level`의 최종 런타임 결합 방식은 다음 단계에서 결정한다. 현재는 기존 Oracle Redaction expression과 호환되도록 표시 예외 구조를 유지한다.
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import java.util.Arrays;
|
||||
@@ -74,6 +75,22 @@ public class PermissionController {
|
||||
.map(column -> column.columnName())
|
||||
.toList()
|
||||
));
|
||||
var maskableColumnsByObject = objects.stream()
|
||||
.collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> protectedObjectService.findColumns(object.objectId()).stream()
|
||||
.filter(ProtectedColumn::sensitive)
|
||||
.map(column -> column.columnName())
|
||||
.toList()
|
||||
));
|
||||
var maskableColumnLabelsByObject = objects.stream()
|
||||
.collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> protectedObjectService.findColumns(object.objectId()).stream()
|
||||
.filter(ProtectedColumn::sensitive)
|
||||
.map(column -> column.columnName() + " [" + column.policyLabel() + "]")
|
||||
.toList()
|
||||
));
|
||||
long columnsAt = System.nanoTime();
|
||||
var dbObjects = protectedObjectService.findDatabaseObjects();
|
||||
long dbObjectsAt = System.nanoTime();
|
||||
@@ -89,6 +106,8 @@ public class PermissionController {
|
||||
model.addAttribute("roles", roles);
|
||||
model.addAttribute("objects", objects);
|
||||
model.addAttribute("columnsByObject", columnsByObject);
|
||||
model.addAttribute("maskableColumnsByObject", maskableColumnsByObject);
|
||||
model.addAttribute("maskableColumnLabelsByObject", maskableColumnLabelsByObject);
|
||||
model.addAttribute("dbObjects", dbObjects);
|
||||
model.addAttribute("permissions", permissions);
|
||||
return "permissions";
|
||||
|
||||
@@ -159,6 +159,38 @@ function renderRuleColumnOptions(columns) {
|
||||
});
|
||||
}
|
||||
|
||||
function renderMaskableColumnOptions(option) {
|
||||
const list = document.querySelector('[data-maskable-column-list]');
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
const columns = (option?.dataset.maskableColumns || '').split(',').filter(Boolean);
|
||||
const labels = (option?.dataset.maskableLabels || '').split('|').filter(Boolean);
|
||||
if (!columns.length) {
|
||||
list.innerHTML = '<span class="text-muted small">선택한 객체에 권한별로 허용할 마스킹 컬럼이 없습니다.</span>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = columns.map((column, index) => (
|
||||
`<button class="btn btn-sm btn-outline-secondary question-preset" type="button" data-mask-column="${column}">`
|
||||
+ `${escapeHtml(labels[index] || column)}</button>`
|
||||
)).join('');
|
||||
list.querySelectorAll('[data-mask-column]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const input = document.querySelector('input[name="visibleColumns"]');
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
const current = input.value.split(',').map((value) => value.trim()).filter(Boolean);
|
||||
const column = button.dataset.maskColumn;
|
||||
if (!current.includes(column)) {
|
||||
current.push(column);
|
||||
}
|
||||
input.value = current.join(', ');
|
||||
updatePermissionWizardPreview(document);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncRuleTypeHints(root = document) {
|
||||
root.querySelectorAll('.rule-row').forEach((row) => {
|
||||
const typeSelect = row.querySelector('.rule-type-select');
|
||||
@@ -198,6 +230,7 @@ async function syncRuleColumnOptions() {
|
||||
}
|
||||
const selectedOption = objectSelect.options[objectSelect.selectedIndex];
|
||||
const fallbackColumns = (selectedOption?.dataset.columns || '').split(',').filter(Boolean);
|
||||
renderMaskableColumnOptions(selectedOption);
|
||||
renderRuleColumnOptions(fallbackColumns);
|
||||
try {
|
||||
const response = await fetch(`/permissions/object-columns?objectRef=${encodeURIComponent(objectSelect.value)}`);
|
||||
@@ -279,8 +312,8 @@ function updatePermissionWizardPreview(root = document) {
|
||||
? `거부 규칙: ${ruleText}`
|
||||
: `허용 규칙: ${ruleText}`;
|
||||
const columnPolicy = visibleColumns
|
||||
? `NULL 처리 예외: ${visibleColumns}`
|
||||
: '민감 컬럼은 NULL 처리 대상';
|
||||
? `이 권한에서 원문 표시 허용: ${visibleColumns}`
|
||||
: '마스킹 대상 컬럼은 기본 정책대로 NULL/마스킹 처리';
|
||||
|
||||
wizard.querySelector('[data-wizard-summary="role"]').textContent = selectedText(roleSelect);
|
||||
wizard.querySelector('[data-wizard-summary="object"]').textContent = selectedText(objectSelect);
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<tr>
|
||||
<td colspan="5" class="policy-source-cell">
|
||||
<details class="column-policy-panel">
|
||||
<summary>컬럼 민감도/마스킹 정책</summary>
|
||||
<summary>기본 컬럼 민감도/마스킹 정책</summary>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
|
||||
@@ -77,7 +77,9 @@
|
||||
<optgroup label="등록된 보호 객체">
|
||||
<option th:each="object : ${objects}"
|
||||
th:value="${'protected:' + object.objectId()}"
|
||||
th:attr="data-columns=${#strings.listJoin(columnsByObject[object.objectId()], ',')}"
|
||||
th:attr="data-columns=${#strings.listJoin(columnsByObject[object.objectId()], ',')},
|
||||
data-maskable-columns=${#strings.listJoin(maskableColumnsByObject[object.objectId()], ',')},
|
||||
data-maskable-labels=${#strings.listJoin(maskableColumnLabelsByObject[object.objectId()], '|')}"
|
||||
th:text="${object.displayName()}"></option>
|
||||
</optgroup>
|
||||
<optgroup label="DB 스키마 객체">
|
||||
@@ -136,15 +138,21 @@
|
||||
<div class="wizard-panel-heading">
|
||||
<span class="wizard-step-number">4</span>
|
||||
<div>
|
||||
<h3>컬럼 NULL 처리 예외</h3>
|
||||
<p>redaction 대상 민감 컬럼 중 원문 표시를 허용할 컬럼을 입력합니다.</p>
|
||||
<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>
|
||||
NULL 처리 예외 컬럼
|
||||
원문 표시 허용 컬럼
|
||||
<input class="form-control" name="visibleColumns" placeholder="CONTENTS">
|
||||
</label>
|
||||
<p class="wizard-hint">쉼표로 여러 컬럼을 입력합니다. 비워두면 민감 컬럼은 NULL 처리 대상으로 남습니다.</p>
|
||||
<p class="wizard-hint">객체의 기본 민감도/마스킹 정책은 ORDS 조회 Handler 대상에서 관리하고, 여기서는 역할 권한에 붙일 원문 표시 예외만 정합니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="wizard-panel" data-wizard-step="5">
|
||||
@@ -161,7 +169,7 @@
|
||||
<div><dt>민감도 상한</dt><dd data-preview="sensitivity">-</dd></div>
|
||||
<div><dt>보호 객체</dt><dd data-preview="object">-</dd></div>
|
||||
<div><dt>행 접근</dt><dd data-preview="rowPolicy">-</dd></div>
|
||||
<div><dt>컬럼 정책</dt><dd data-preview="columnPolicy">-</dd></div>
|
||||
<div><dt>권한별 컬럼 마스킹</dt><dd data-preview="columnPolicy">-</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,7 +195,7 @@
|
||||
<th>Effect</th>
|
||||
<th>행 규칙</th>
|
||||
<th>적용 필터</th>
|
||||
<th>NULL 제외 컬럼</th>
|
||||
<th>원문 표시 허용 컬럼</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
Reference in New Issue
Block a user