[UX] #605 make permission wizard safety review actionable
This commit is contained in:
@@ -770,6 +770,10 @@ function updatePermissionWizardPreview(root = document) {
|
||||
const rules = collectWizardRules(wizard);
|
||||
const predicates = collectWizardPredicates(wizard);
|
||||
const ruleText = rules.length ? rules.join(', ') : '행 규칙 없음';
|
||||
const hasRole = Boolean(roleSelect?.value);
|
||||
const hasObject = Boolean(objectSelect?.value);
|
||||
const hasAllRule = rules.includes('ALL');
|
||||
const hasAllRuleConflict = hasAllRule && rules.length > 1;
|
||||
const directUsers = splitList(roleOption?.dataset.directUsers || '');
|
||||
const groups = splitList(roleOption?.dataset.groups || '');
|
||||
const groupUsers = splitList(roleOption?.dataset.groupUsers || '');
|
||||
@@ -790,11 +794,23 @@ function updatePermissionWizardPreview(root = document) {
|
||||
? '마스킹 대상 컬럼 없음'
|
||||
: (nullColumns.length ? `NULL 처리: ${nullColumns.join(', ')}` : '선택한 마스킹 컬럼 모두 원문 표시 허용');
|
||||
const affectedPrincipals = `직접 사용자 ${directUsers.length}명 / 그룹 ${groups.length}개 / 그룹 상속 사용자 ${groupUsers.length}명`;
|
||||
const readiness = !hasRole
|
||||
? '역할을 선택하세요'
|
||||
: !hasObject
|
||||
? '보호 객체를 선택하세요'
|
||||
: hasAllRuleConflict
|
||||
? 'ALL 규칙을 단독으로 정리하세요'
|
||||
: '저장 전 검토 가능';
|
||||
const saveGuard = hasAllRule
|
||||
? `${effect === 'DENY' ? '전체 행 거부' : '전체 행 허용'}입니다. ${affectedPrincipals}에게 영향을 줄 수 있습니다.`
|
||||
: `${effect === 'DENY' ? '선택 조건의 행을 거부' : '선택 조건의 행만 허용'}합니다. 저장 후 결과 확인에서 실제 VPD predicate를 검증하세요.`;
|
||||
|
||||
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-wizard-summary="affected"]').textContent = hasRole ? affectedPrincipals : '역할 선택 후 확인';
|
||||
wizard.querySelector('[data-wizard-summary="readiness"]').textContent = readiness;
|
||||
|
||||
setWizardPreview(wizard, 'role', selectedText(roleSelect));
|
||||
setWizardPreview(wizard, 'sensitivity', roleOption?.dataset.maxSensitivity || 'PUBLIC');
|
||||
@@ -810,6 +826,84 @@ function updatePermissionWizardPreview(root = document) {
|
||||
setWizardPreview(wizard, 'predicatePreview', predicateText);
|
||||
setWizardPreview(wizard, 'columnPolicy', columnPolicy);
|
||||
setWizardPreview(wizard, 'nullPolicy', nullPolicy);
|
||||
setWizardPreview(wizard, 'saveGuard', hasRole && hasObject ? saveGuard : '역할과 보호 객체를 선택하면 저장 영향을 계산합니다.');
|
||||
}
|
||||
|
||||
function permissionWizardIndicators(wizard) {
|
||||
return wizard.closest('.content-band')?.querySelectorAll('[data-wizard-target]') || [];
|
||||
}
|
||||
|
||||
function showPermissionWizardValidation(wizard, message) {
|
||||
const validation = wizard.querySelector('[data-wizard-validation]');
|
||||
if (!validation) {
|
||||
return;
|
||||
}
|
||||
validation.textContent = message;
|
||||
validation.hidden = false;
|
||||
}
|
||||
|
||||
function clearPermissionWizardValidation(wizard) {
|
||||
const validation = wizard.querySelector('[data-wizard-validation]');
|
||||
if (validation) {
|
||||
validation.hidden = true;
|
||||
validation.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
function permissionWizardRuleValidationMessage(wizard) {
|
||||
const rows = Array.from(wizard.querySelectorAll('.rule-row'));
|
||||
if (!rows.length) {
|
||||
return '행 규칙을 하나 이상 추가하세요.';
|
||||
}
|
||||
const values = rows.map((row) => ({
|
||||
column: row.querySelector('[name="ruleColumn"]')?.value || '',
|
||||
type: row.querySelector('[name="ruleType"]')?.value || '',
|
||||
value: row.querySelector('[name="ruleValue"]')?.value.trim() || ''
|
||||
}));
|
||||
if (values.some((rule) => rule.type === 'ALL') && values.length > 1) {
|
||||
return 'ALL 규칙은 다른 조건 규칙과 함께 저장할 수 없습니다. ALL만 남기거나 ALL을 삭제하세요.';
|
||||
}
|
||||
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG'];
|
||||
const seen = new Set();
|
||||
for (const [index, rule] of values.entries()) {
|
||||
const position = index + 1;
|
||||
if (!rule.type) {
|
||||
return `${position}번째 행 규칙 유형을 선택하세요.`;
|
||||
}
|
||||
if (['=', '!='].includes(rule.type) && !rule.column) {
|
||||
return `${position}번째 ${rule.type} 규칙에는 비교할 컬럼이 필요합니다.`;
|
||||
}
|
||||
if (valueRequired.includes(rule.type) && !rule.value) {
|
||||
return `${position}번째 ${rule.type} 규칙의 값을 입력하세요.`;
|
||||
}
|
||||
if (rule.type === 'TAG' && rule.value && !/^[A-Za-z0-9_-]+$/.test(rule.value)) {
|
||||
return `${position}번째 TAG 값은 영문·숫자, '_' 또는 '-'만 사용할 수 있습니다.`;
|
||||
}
|
||||
const signature = `${rule.column}:${rule.type}:${rule.value.toUpperCase()}`;
|
||||
if (seen.has(signature)) {
|
||||
return `${position}번째 행 규칙이 앞의 규칙과 중복됩니다.`;
|
||||
}
|
||||
seen.add(signature);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function validatePermissionWizardStep(wizard, step) {
|
||||
if (step === 1 && !wizard.querySelector('[name="roleId"]')?.value) {
|
||||
return '권한을 적용할 역할을 선택하세요.';
|
||||
}
|
||||
if (step === 2 && !wizard.querySelector('[name="objectRef"]')?.value) {
|
||||
return '권한을 적용할 보호 객체를 선택하세요.';
|
||||
}
|
||||
if (step === 3) {
|
||||
return permissionWizardRuleValidationMessage(wizard);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function focusPermissionWizardStep(wizard, step) {
|
||||
const panel = wizard.querySelector(`[data-wizard-step="${step}"]`);
|
||||
panel?.querySelector('select, input, button')?.focus();
|
||||
}
|
||||
|
||||
function activatePermissionWizardStep(wizard, step) {
|
||||
@@ -819,8 +913,10 @@ function activatePermissionWizardStep(wizard, step) {
|
||||
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);
|
||||
permissionWizardIndicators(wizard).forEach((button) => {
|
||||
const active = Number(button.dataset.wizardTarget) === nextStep;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-current', active ? 'step' : 'false');
|
||||
});
|
||||
wizard.dataset.currentStep = String(nextStep);
|
||||
const prev = wizard.querySelector('[data-wizard-prev]');
|
||||
@@ -838,6 +934,27 @@ function activatePermissionWizardStep(wizard, step) {
|
||||
updatePermissionWizardPreview(document);
|
||||
}
|
||||
|
||||
function movePermissionWizard(wizard, targetStep) {
|
||||
const currentStep = Number(wizard.dataset.currentStep || '1');
|
||||
if (targetStep <= currentStep) {
|
||||
clearPermissionWizardValidation(wizard);
|
||||
activatePermissionWizardStep(wizard, targetStep);
|
||||
return true;
|
||||
}
|
||||
for (let step = 1; step < targetStep; step += 1) {
|
||||
const message = validatePermissionWizardStep(wizard, step);
|
||||
if (message) {
|
||||
activatePermissionWizardStep(wizard, step);
|
||||
showPermissionWizardValidation(wizard, message);
|
||||
focusPermissionWizardStep(wizard, step);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
clearPermissionWizardValidation(wizard);
|
||||
activatePermissionWizardStep(wizard, targetStep);
|
||||
return true;
|
||||
}
|
||||
|
||||
function initPermissionWizard() {
|
||||
const wizard = document.querySelector('[data-permission-wizard]');
|
||||
if (!wizard) {
|
||||
@@ -845,16 +962,34 @@ function initPermissionWizard() {
|
||||
}
|
||||
wizard.dataset.currentStep = wizard.dataset.currentStep || '1';
|
||||
wizard.querySelector('[data-wizard-prev]')?.addEventListener('click', () => {
|
||||
activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1') - 1);
|
||||
movePermissionWizard(wizard, Number(wizard.dataset.currentStep || '1') - 1);
|
||||
});
|
||||
wizard.querySelector('[data-wizard-next]')?.addEventListener('click', () => {
|
||||
activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1') + 1);
|
||||
movePermissionWizard(wizard, Number(wizard.dataset.currentStep || '1') + 1);
|
||||
});
|
||||
document.querySelectorAll('[data-wizard-target]').forEach((button) => {
|
||||
button.addEventListener('click', () => activatePermissionWizardStep(wizard, Number(button.dataset.wizardTarget)));
|
||||
permissionWizardIndicators(wizard).forEach((button) => {
|
||||
button.addEventListener('click', () => movePermissionWizard(wizard, Number(button.dataset.wizardTarget)));
|
||||
});
|
||||
wizard.addEventListener('input', () => {
|
||||
clearPermissionWizardValidation(wizard);
|
||||
updatePermissionWizardPreview(document);
|
||||
});
|
||||
wizard.addEventListener('change', () => {
|
||||
clearPermissionWizardValidation(wizard);
|
||||
updatePermissionWizardPreview(document);
|
||||
});
|
||||
wizard.addEventListener('submit', (event) => {
|
||||
for (let step = 1; step <= 3; step += 1) {
|
||||
const message = validatePermissionWizardStep(wizard, step);
|
||||
if (message) {
|
||||
event.preventDefault();
|
||||
activatePermissionWizardStep(wizard, step);
|
||||
showPermissionWizardValidation(wizard, message);
|
||||
focusPermissionWizardStep(wizard, step);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
wizard.addEventListener('input', () => updatePermissionWizardPreview(document));
|
||||
wizard.addEventListener('change', () => updatePermissionWizardPreview(document));
|
||||
activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1'));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user