Improve data access control UX guidance

This commit is contained in:
devmrko
2026-07-13 23:14:51 +09:00
parent e18b30feab
commit a40102a79a
10 changed files with 343 additions and 5 deletions

View File

@@ -0,0 +1,141 @@
package com.cloudhandson.vpdbackoffice.domain.operation;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus;
import java.util.List;
/**
* Page-level health summary computed from the same DB-backed rows shown in
* operation-status.html. It intentionally does not hide the row-level evidence:
* the summary is only a triage layer for operators.
*/
public record OperationHealthSummary(
int protectedObjectCount,
int rowPolicyOkCount,
int rowPolicyWarnCount,
int rowPolicyErrorCount,
int missingHandlerCount,
int missingPolicyCount,
int invalidFunctionCount,
int unverifiedCount,
int maskingPolicyCount,
int maskingAppliedCount,
int maskingInactiveExpectedCount,
int maskingMismatchCount
) {
public static OperationHealthSummary from(
List<OperationStatusRow> rows,
List<MaskingPolicyStatus> maskingStatuses
) {
int rowPolicyOk = 0;
int rowPolicyWarn = 0;
int rowPolicyError = 0;
int missingHandler = 0;
int missingPolicy = 0;
int invalidFunction = 0;
int unverified = 0;
for (OperationStatusRow row : rows) {
switch (row.healthLevel()) {
case "OK" -> rowPolicyOk++;
case "ERROR" -> rowPolicyError++;
default -> rowPolicyWarn++;
}
if (row.handlerId() == null) {
missingHandler++;
}
if (row.policyNames() == null || row.policyNames().isBlank()) {
missingPolicy++;
}
if (row.functionStatus() != null && !"VALID".equalsIgnoreCase(row.functionStatus())) {
invalidFunction++;
}
if (row.lastProbeStatus() == null || row.lastProbeStatus().isBlank()) {
unverified++;
}
}
int maskingApplied = 0;
int maskingInactive = 0;
int maskingMismatch = 0;
for (MaskingPolicyStatus status : maskingStatuses) {
if (status.applied()) {
maskingApplied++;
} else if (status.inactiveAsExpected()) {
maskingInactive++;
} else {
maskingMismatch++;
}
}
return new OperationHealthSummary(
rows.size(),
rowPolicyOk,
rowPolicyWarn,
rowPolicyError,
missingHandler,
missingPolicy,
invalidFunction,
unverified,
maskingStatuses.size(),
maskingApplied,
maskingInactive,
maskingMismatch
);
}
public String overallLevel() {
if (rowPolicyErrorCount > 0 || invalidFunctionCount > 0 || maskingMismatchCount > 0) {
return "ERROR";
}
if (rowPolicyWarnCount > 0 || missingHandlerCount > 0 || missingPolicyCount > 0
|| unverifiedCount > 0) {
return "WARN";
}
return "OK";
}
public String overallLabel() {
return switch (overallLevel()) {
case "ERROR" -> "조치 필요";
case "WARN" -> "확인 필요";
default -> "정상";
};
}
public String overallBadgeClass() {
return switch (overallLevel()) {
case "ERROR" -> "text-bg-danger";
case "WARN" -> "text-bg-warning";
default -> "text-bg-success";
};
}
public String rowPolicySummary() {
return "정상 " + rowPolicyOkCount + " · 확인 " + rowPolicyWarnCount + " · 오류 " + rowPolicyErrorCount;
}
public String maskingSummary() {
return "적용 " + maskingAppliedCount + " · 미적용 정상 " + maskingInactiveExpectedCount
+ " · 불일치 " + maskingMismatchCount;
}
public String primaryAction() {
if (invalidFunctionCount > 0) {
return "컴파일 오류가 있는 VPD Filter를 먼저 확인하세요.";
}
if (maskingMismatchCount > 0) {
return "컬럼 마스킹 화면에서 DB ASO 정책 동기화를 확인하세요.";
}
if (missingPolicyCount > 0) {
return "보호 상태에서 행 접근 정책(VPD)을 적용하세요.";
}
if (missingHandlerCount > 0) {
return "조회 대상 또는 조회 연동에서 ORDS handler를 확인하세요.";
}
if (unverifiedCount > 0) {
return "접근 검증을 실행해 실제 결과를 확인하세요.";
}
return "현재 요약 기준으로 즉시 조치할 항목은 없습니다.";
}
}