feat #468: add permission setup wizard
This commit is contained in:
34
docs/design/468-permission-wizard/README.md
Normal file
34
docs/design/468-permission-wizard/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# #468 권한 등록 Wizard
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
권한 등록은 역할, 보호 객체, 허용/거부, 행 규칙, 컬럼 redaction 예외를 한 번에 설정해야 한다. 기존 단일 폼은 빠르게 입력할 수 있지만 신규 운영자가 순서를 놓치기 쉽고, 저장 전에 최종 정책 의미를 확인하기 어렵다.
|
||||||
|
|
||||||
|
## 결정
|
||||||
|
|
||||||
|
- 서버 저장 API(`/permissions`)는 유지한다.
|
||||||
|
- 권한 추가 영역만 단계형 wizard UI로 재구성한다.
|
||||||
|
- 기존 권한 목록은 그대로 유지한다.
|
||||||
|
- 단계는 다음 순서로 고정한다.
|
||||||
|
1. 역할 선택
|
||||||
|
2. 보호 객체 선택
|
||||||
|
3. 권한 효과와 행 규칙
|
||||||
|
4. 컬럼 NULL 처리 예외
|
||||||
|
5. 저장 전 검토
|
||||||
|
- 저장 전 검토 단계에서 effective policy preview를 표시한다.
|
||||||
|
|
||||||
|
## Preview 규칙
|
||||||
|
|
||||||
|
- `ALLOW + ALL`: 모든 행 조회 허용
|
||||||
|
- `ALLOW + 조건`: 조건에 맞는 행만 조회 허용
|
||||||
|
- `DENY + ALL`: 해당 역할의 객체 조회 거부
|
||||||
|
- `DENY + 조건`: 조건에 맞는 행을 제외
|
||||||
|
- 컬럼 예외가 비어 있으면 민감 컬럼은 NULL 처리 대상으로 설명한다.
|
||||||
|
- 컬럼 예외가 있으면 해당 컬럼을 NULL 처리 예외로 표시한다.
|
||||||
|
|
||||||
|
## 완료 기준
|
||||||
|
|
||||||
|
- `/permissions`에서 단계 이동과 요약 갱신이 동작한다.
|
||||||
|
- 기존 저장 POST parameter 이름은 유지한다.
|
||||||
|
- 행 규칙 컬럼 동적 조회와 규칙 추가/삭제가 계속 동작한다.
|
||||||
|
- Maven 테스트와 인증 화면 렌더링 확인을 통과한다.
|
||||||
@@ -336,6 +336,119 @@ body {
|
|||||||
grid-template-columns: 1fr 1fr 1fr auto;
|
grid-template-columns: 1fr 1fr 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.permission-wizard {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-step-indicator {
|
||||||
|
background: var(--rw-surface-muted);
|
||||||
|
border: 1px solid var(--rw-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--rw-muted);
|
||||||
|
font-size: .82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: .3rem .7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-step-indicator.active {
|
||||||
|
background: var(--rw-primary);
|
||||||
|
border-color: var(--rw-primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-summary {
|
||||||
|
display: grid;
|
||||||
|
gap: .75rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-summary > div,
|
||||||
|
.policy-preview dl > div {
|
||||||
|
background: var(--rw-surface-muted);
|
||||||
|
border: 1px solid var(--rw-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-summary span,
|
||||||
|
.policy-preview dt {
|
||||||
|
color: var(--rw-muted);
|
||||||
|
display: block;
|
||||||
|
font-size: .78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-summary strong,
|
||||||
|
.policy-preview dd {
|
||||||
|
display: block;
|
||||||
|
margin: .25rem 0 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-panel {
|
||||||
|
border: 1px solid var(--rw-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-panel.active {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-panel-heading {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
gap: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-panel-heading h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-panel-heading p,
|
||||||
|
.wizard-hint {
|
||||||
|
color: var(--rw-muted);
|
||||||
|
margin: .15rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-step-number {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--rw-primary-soft);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--rw-primary);
|
||||||
|
display: inline-flex;
|
||||||
|
font-weight: 800;
|
||||||
|
height: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-actions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-preview dl {
|
||||||
|
display: grid;
|
||||||
|
gap: .75rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.question-presets {
|
.question-presets {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -233,6 +233,113 @@ async function syncObjectCatalogSelection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectedText(select) {
|
||||||
|
if (!select || select.selectedIndex < 0) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return select.options[select.selectedIndex].textContent.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectWizardRules(root) {
|
||||||
|
return Array.from(root.querySelectorAll('.rule-row')).map((row) => {
|
||||||
|
const column = row.querySelector('[name="ruleColumn"]')?.value || '';
|
||||||
|
const type = row.querySelector('[name="ruleType"]')?.value || '';
|
||||||
|
const value = row.querySelector('[name="ruleValue"]')?.value || '';
|
||||||
|
const displayColumn = column || {
|
||||||
|
MY_DEPT: 'DEPT_CODE',
|
||||||
|
SELF: 'OWNER_EMP_NO',
|
||||||
|
DEPT: 'DEPT_CODE',
|
||||||
|
EMP_NO: 'OWNER_EMP_NO',
|
||||||
|
ALL: ''
|
||||||
|
}[type] || '';
|
||||||
|
if (type === 'ALL') {
|
||||||
|
return 'ALL';
|
||||||
|
}
|
||||||
|
if (['MY_DEPT', 'SELF'].includes(type)) {
|
||||||
|
return `${displayColumn} ${type}`;
|
||||||
|
}
|
||||||
|
return [displayColumn, type, value].filter(Boolean).join(' ');
|
||||||
|
}).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePermissionWizardPreview(root = document) {
|
||||||
|
const wizard = root.querySelector('[data-permission-wizard]');
|
||||||
|
if (!wizard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roleSelect = wizard.querySelector('[name="roleId"]');
|
||||||
|
const objectSelect = wizard.querySelector('[name="objectRef"]');
|
||||||
|
const effectSelect = wizard.querySelector('[name="permissionEffect"]');
|
||||||
|
const visibleColumns = wizard.querySelector('[name="visibleColumns"]')?.value.trim() || '';
|
||||||
|
const roleOption = roleSelect?.options[roleSelect.selectedIndex];
|
||||||
|
const effect = effectSelect?.value || 'ALLOW';
|
||||||
|
const rules = collectWizardRules(wizard);
|
||||||
|
const ruleText = rules.length ? rules.join(', ') : '행 규칙 없음';
|
||||||
|
const rowPolicy = effect === 'DENY'
|
||||||
|
? `거부 규칙: ${ruleText}`
|
||||||
|
: `허용 규칙: ${ruleText}`;
|
||||||
|
const columnPolicy = visibleColumns
|
||||||
|
? `NULL 처리 예외: ${visibleColumns}`
|
||||||
|
: '민감 컬럼은 NULL 처리 대상';
|
||||||
|
|
||||||
|
wizard.querySelector('[data-wizard-summary="role"]').textContent = selectedText(roleSelect);
|
||||||
|
wizard.querySelector('[data-wizard-summary="object"]').textContent = selectedText(objectSelect);
|
||||||
|
wizard.querySelector('[data-wizard-summary="effect"]').textContent = effect;
|
||||||
|
wizard.querySelector('[data-wizard-summary="rules"]').textContent = ruleText;
|
||||||
|
|
||||||
|
wizard.querySelector('[data-preview="role"]').textContent = selectedText(roleSelect);
|
||||||
|
wizard.querySelector('[data-preview="sensitivity"]').textContent = roleOption?.dataset.maxSensitivity || 'PUBLIC';
|
||||||
|
wizard.querySelector('[data-preview="object"]').textContent = selectedText(objectSelect);
|
||||||
|
wizard.querySelector('[data-preview="rowPolicy"]').textContent = rowPolicy;
|
||||||
|
wizard.querySelector('[data-preview="columnPolicy"]').textContent = columnPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activatePermissionWizardStep(wizard, step) {
|
||||||
|
const panels = Array.from(wizard.querySelectorAll('[data-wizard-step]'));
|
||||||
|
const maxStep = panels.length;
|
||||||
|
const nextStep = Math.max(1, Math.min(step, maxStep));
|
||||||
|
panels.forEach((panel) => {
|
||||||
|
panel.classList.toggle('active', Number(panel.dataset.wizardStep) === nextStep);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-wizard-target]').forEach((button) => {
|
||||||
|
button.classList.toggle('active', Number(button.dataset.wizardTarget) === nextStep);
|
||||||
|
});
|
||||||
|
wizard.dataset.currentStep = String(nextStep);
|
||||||
|
const prev = wizard.querySelector('[data-wizard-prev]');
|
||||||
|
const next = wizard.querySelector('[data-wizard-next]');
|
||||||
|
const submit = wizard.querySelector('[data-wizard-submit]');
|
||||||
|
if (prev) {
|
||||||
|
prev.disabled = nextStep === 1;
|
||||||
|
}
|
||||||
|
if (next) {
|
||||||
|
next.hidden = nextStep === maxStep;
|
||||||
|
}
|
||||||
|
if (submit) {
|
||||||
|
submit.hidden = nextStep !== maxStep;
|
||||||
|
}
|
||||||
|
updatePermissionWizardPreview(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPermissionWizard() {
|
||||||
|
const wizard = document.querySelector('[data-permission-wizard]');
|
||||||
|
if (!wizard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wizard.dataset.currentStep = wizard.dataset.currentStep || '1';
|
||||||
|
wizard.querySelector('[data-wizard-prev]')?.addEventListener('click', () => {
|
||||||
|
activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1') - 1);
|
||||||
|
});
|
||||||
|
wizard.querySelector('[data-wizard-next]')?.addEventListener('click', () => {
|
||||||
|
activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1') + 1);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-wizard-target]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => activatePermissionWizardStep(wizard, Number(button.dataset.wizardTarget)));
|
||||||
|
});
|
||||||
|
wizard.addEventListener('input', () => updatePermissionWizardPreview(document));
|
||||||
|
wizard.addEventListener('change', () => updatePermissionWizardPreview(document));
|
||||||
|
activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1'));
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
renderMarkdownViews();
|
renderMarkdownViews();
|
||||||
const master = document.getElementById('userRoleMaster');
|
const master = document.getElementById('userRoleMaster');
|
||||||
@@ -242,11 +349,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
const objectSelect = document.querySelector('select[name="objectRef"]');
|
const objectSelect = document.querySelector('select[name="objectRef"]');
|
||||||
if (objectSelect) {
|
if (objectSelect) {
|
||||||
objectSelect.addEventListener('change', syncRuleColumnOptions);
|
objectSelect.addEventListener('change', () => {
|
||||||
|
syncRuleColumnOptions();
|
||||||
|
updatePermissionWizardPreview(document);
|
||||||
|
});
|
||||||
syncRuleColumnOptions();
|
syncRuleColumnOptions();
|
||||||
}
|
}
|
||||||
|
initPermissionWizard();
|
||||||
document.querySelectorAll('.rule-type-select').forEach((select) => {
|
document.querySelectorAll('.rule-type-select').forEach((select) => {
|
||||||
select.addEventListener('change', () => syncRuleTypeHints());
|
select.addEventListener('change', () => {
|
||||||
|
syncRuleTypeHints();
|
||||||
|
updatePermissionWizardPreview(document);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
syncRuleTypeHints();
|
syncRuleTypeHints();
|
||||||
const catalog = document.getElementById('objectCatalogSelect');
|
const catalog = document.getElementById('objectCatalogSelect');
|
||||||
@@ -264,15 +378,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
clone.querySelectorAll('input').forEach((input) => input.value = '');
|
clone.querySelectorAll('input').forEach((input) => input.value = '');
|
||||||
const cloneButton = clone.querySelector('[data-rule-add]');
|
const cloneButton = clone.querySelector('[data-rule-add]');
|
||||||
cloneButton.textContent = '삭제';
|
cloneButton.textContent = '삭제';
|
||||||
cloneButton.addEventListener('click', () => clone.remove());
|
cloneButton.addEventListener('click', () => {
|
||||||
|
clone.remove();
|
||||||
|
updatePermissionWizardPreview(document);
|
||||||
|
});
|
||||||
clone.querySelectorAll('.rule-type-select').forEach((select) => {
|
clone.querySelectorAll('.rule-type-select').forEach((select) => {
|
||||||
select.addEventListener('change', () => syncRuleTypeHints());
|
select.addEventListener('change', () => {
|
||||||
|
syncRuleTypeHints();
|
||||||
|
updatePermissionWizardPreview(document);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
list.appendChild(clone);
|
list.appendChild(clone);
|
||||||
syncRuleColumnOptions();
|
syncRuleColumnOptions();
|
||||||
syncRuleTypeHints(clone);
|
syncRuleTypeHints(clone);
|
||||||
|
updatePermissionWizardPreview(document);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
updatePermissionWizardPreview(document);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||||
|
|||||||
@@ -12,15 +12,65 @@
|
|||||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||||
|
|
||||||
<section class="content-band">
|
<section class="content-band">
|
||||||
<h2>권한 추가</h2>
|
<div class="section-heading">
|
||||||
<form method="post" action="/permissions" class="form-grid">
|
<h2>권한 추가 Wizard</h2>
|
||||||
|
<div class="wizard-progress" aria-label="권한 추가 단계">
|
||||||
|
<button class="wizard-step-indicator active" type="button" data-wizard-target="1">1 역할</button>
|
||||||
|
<button class="wizard-step-indicator" type="button" data-wizard-target="2">2 객체</button>
|
||||||
|
<button class="wizard-step-indicator" type="button" data-wizard-target="3">3 행 규칙</button>
|
||||||
|
<button class="wizard-step-indicator" type="button" data-wizard-target="4">4 컬럼</button>
|
||||||
|
<button class="wizard-step-indicator" type="button" data-wizard-target="5">5 검토</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/permissions" class="permission-wizard" data-permission-wizard>
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<aside class="wizard-summary" aria-live="polite">
|
||||||
|
<div>
|
||||||
|
<span>역할</span>
|
||||||
|
<strong data-wizard-summary="role">선택 전</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>객체</span>
|
||||||
|
<strong data-wizard-summary="object">선택 전</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>효과</span>
|
||||||
|
<strong data-wizard-summary="effect">ALLOW</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>행 규칙</span>
|
||||||
|
<strong data-wizard-summary="rules">ALL</strong>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="wizard-panel active" data-wizard-step="1">
|
||||||
|
<div class="wizard-panel-heading">
|
||||||
|
<span class="wizard-step-number">1</span>
|
||||||
|
<div>
|
||||||
|
<h3>역할 선택</h3>
|
||||||
|
<p>권한을 부여하거나 거부할 역할을 선택합니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<label>
|
<label>
|
||||||
역할
|
역할
|
||||||
<select class="form-select" name="roleId" required>
|
<select class="form-select" name="roleId" required>
|
||||||
<option th:each="role : ${roles}" th:value="${role.roleId()}" th:text="${role.roleName()}"></option>
|
<option th:each="role : ${roles}"
|
||||||
|
th:value="${role.roleId()}"
|
||||||
|
th:text="${role.roleName()}"
|
||||||
|
th:attr="data-max-sensitivity=${role.maxSensitivityLevel()}"></option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<p class="wizard-hint">선택한 역할의 민감도 허용 상한도 preview에 반영됩니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-panel" data-wizard-step="2">
|
||||||
|
<div class="wizard-panel-heading">
|
||||||
|
<span class="wizard-step-number">2</span>
|
||||||
|
<div>
|
||||||
|
<h3>보호 객체 선택</h3>
|
||||||
|
<p>등록된 보호 객체 또는 DB 스키마 객체를 선택합니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<label>
|
<label>
|
||||||
보호 객체
|
보호 객체
|
||||||
<select class="form-select" name="objectRef" required>
|
<select class="form-select" name="objectRef" required>
|
||||||
@@ -37,6 +87,17 @@
|
|||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<p class="wizard-hint">DB 스키마 객체를 선택하면 저장 시 보호 객체가 자동 등록됩니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-panel" data-wizard-step="3">
|
||||||
|
<div class="wizard-panel-heading">
|
||||||
|
<span class="wizard-step-number">3</span>
|
||||||
|
<div>
|
||||||
|
<h3>권한 효과와 행 규칙</h3>
|
||||||
|
<p>허용/거부 방향과 VPD 행 필터 조건을 설정합니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<label>
|
<label>
|
||||||
권한 효과
|
권한 효과
|
||||||
<select class="form-select" name="permissionEffect" required>
|
<select class="form-select" name="permissionEffect" required>
|
||||||
@@ -44,7 +105,7 @@
|
|||||||
<option value="DENY">거부</option>
|
<option value="DENY">거부</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div class="span-2">
|
<div>
|
||||||
<div class="field-block-title">행 규칙</div>
|
<div class="field-block-title">행 규칙</div>
|
||||||
<div id="rowRuleList" class="rule-list">
|
<div id="rowRuleList" class="rule-list">
|
||||||
<div class="rule-row">
|
<div class="rule-row">
|
||||||
@@ -69,11 +130,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="span-2">
|
|
||||||
<div class="field-block-title">컬럼 NULL 처리 예외</div>
|
|
||||||
<input class="form-control" name="visibleColumns" placeholder="CONTENTS">
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" type="submit">저장</button>
|
|
||||||
|
<div class="wizard-panel" data-wizard-step="4">
|
||||||
|
<div class="wizard-panel-heading">
|
||||||
|
<span class="wizard-step-number">4</span>
|
||||||
|
<div>
|
||||||
|
<h3>컬럼 NULL 처리 예외</h3>
|
||||||
|
<p>redaction 대상 민감 컬럼 중 원문 표시를 허용할 컬럼을 입력합니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
NULL 처리 예외 컬럼
|
||||||
|
<input class="form-control" name="visibleColumns" placeholder="CONTENTS">
|
||||||
|
</label>
|
||||||
|
<p class="wizard-hint">쉼표로 여러 컬럼을 입력합니다. 비워두면 민감 컬럼은 NULL 처리 대상으로 남습니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-panel" data-wizard-step="5">
|
||||||
|
<div class="wizard-panel-heading">
|
||||||
|
<span class="wizard-step-number">5</span>
|
||||||
|
<div>
|
||||||
|
<h3>저장 전 검토</h3>
|
||||||
|
<p>저장될 권한과 예상 effective policy를 확인합니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="policy-preview" data-policy-preview>
|
||||||
|
<dl>
|
||||||
|
<div><dt>역할</dt><dd data-preview="role">-</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="rowPolicy">-</dd></div>
|
||||||
|
<div><dt>컬럼 정책</dt><dd data-preview="columnPolicy">-</dd></div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard-actions">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" data-wizard-prev disabled>이전</button>
|
||||||
|
<button class="btn btn-outline-primary" type="button" data-wizard-next>다음</button>
|
||||||
|
<button class="btn btn-primary" type="submit" data-wizard-submit hidden>저장</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user