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