diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/operation/OperationHealthSummary.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/operation/OperationHealthSummary.java new file mode 100644 index 0000000..b858ccc --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/operation/OperationHealthSummary.java @@ -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 rows, + List 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 "현재 요약 기준으로 즉시 조치할 항목은 없습니다."; + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/OperationStatusController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/OperationStatusController.java index c13e761..3fa9206 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/OperationStatusController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/OperationStatusController.java @@ -1,5 +1,6 @@ package com.cloudhandson.vpdbackoffice.web; +import com.cloudhandson.vpdbackoffice.domain.operation.OperationHealthSummary; import com.cloudhandson.vpdbackoffice.service.MaskingRuleService; import com.cloudhandson.vpdbackoffice.service.OperationStatusService; import org.springframework.stereotype.Controller; @@ -19,8 +20,11 @@ public class OperationStatusController { @GetMapping("/operation-status") public String status(Model model) { - model.addAttribute("rows", service.findRows()); - model.addAttribute("maskingPolicyStatuses", maskingRuleService.findPolicyStatuses()); + var rows = service.findRows(); + var maskingPolicyStatuses = maskingRuleService.findPolicyStatuses(); + model.addAttribute("rows", rows); + model.addAttribute("maskingPolicyStatuses", maskingPolicyStatuses); + model.addAttribute("summary", OperationHealthSummary.from(rows, maskingPolicyStatuses)); return "operation-status"; } } diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index d61deaf..6ab02b1 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -2222,6 +2222,27 @@ body { margin: .4rem 0 1rem; } +.guided-check-list { + background: var(--rw-surface-muted); + border: 1px solid var(--rw-border); + border-radius: 10px; + color: var(--rw-muted); + display: grid; + gap: .4rem; + margin: .8rem 0 1rem; + padding: .85rem .95rem .85rem 1.8rem; +} + +.guided-check-list li { + line-height: 1.45; +} + +.guided-check-list a { + color: var(--rw-primary); + font-weight: 800; + text-decoration: none; +} + .dashboard-action-grid { display: grid; gap: .7rem; diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 5d29467..353e201 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -116,6 +116,36 @@
./run.sh backoffice-support
+
+
+
+ 전체 그림 +

토큰은 사용자를 식별하고, DB 정책이 행과 컬럼을 나눠서 제어합니다

+

이 백오피스는 토큰 자체에 권한을 복사하지 않습니다. 요청 시점에 토큰으로 사용자를 찾고, 저장된 역할·규칙을 DB 세션 context와 VPD/ASO 정책에 반영합니다.

+
+
+
+
+
+
1. 사용자 식별
+
Bearer Token으로 CB_AGENT_CTX에 사용자·부서·이해관계자 정보를 설정합니다.
+
+
+
2. 행 접근(VPD)
+
행 접근 규칙이 대상 테이블의 WHERE predicate로 변환되어 볼 수 있는 행만 남깁니다.
+
+
+
3. 컬럼 마스킹(ASO)
+
허용된 행 안에서 민감 컬럼을 원문으로 줄지, 마스킹해서 줄지 결정합니다.
+
+
+
4. 접근 검증
+
토큰으로 실제 ORDS 조회를 실행하고, 결과 행·마스킹 컬럼·감사 증적을 확인합니다.
+
+
+
+
+
업무 흐름 @@ -132,6 +162,13 @@

바로 시작

+
    +
  1. 사용자역할을 준비합니다.
  2. +
  3. 행 접근 규칙에서 역할별 대상 객체와 행 조건을 저장합니다.
  4. +
  5. 보호 상태에서 대상 테이블의 VPD 연결 상태를 확인합니다.
  6. +
  7. 컬럼 마스킹에서 민감 컬럼의 ASO 정책을 연결합니다.
  8. +
  9. 검증 세션을 발급하고 접근 검증에서 실제 결과를 확인합니다.
  10. +
행 접근 규칙역할별 객체·행 조건을 설정합니다. 컬럼은 ASO 마스킹에서 관리합니다. diff --git a/src/main/resources/templates/fragments/probe-result.html b/src/main/resources/templates/fragments/probe-result.html index 91365a6..a645999 100644 --- a/src/main/resources/templates/fragments/probe-result.html +++ b/src/main/resources/templates/fragments/probe-result.html @@ -16,6 +16,35 @@

결과 설명

+
+
+

검증 흐름 요약

+ 토큰 → VPD → ASO → FGA +
+
+
+
+
1. 토큰 해석
+
사용자 확인
+
유효한 사용자로 해석되지 않았습니다. 이 경우 DB context가 설정되지 않아 권한이 없습니다.
+
+
+
2. 행 접근(VPD)
+
행 접근 결과
+
+
+
3. 컬럼 마스킹(ASO)
+
마스킹 컬럼
+
응답에서 마스킹 또는 NULL로 관찰된 민감 컬럼이 없습니다. 원문 허용이거나 해당 컬럼이 결과에 없을 수 있습니다.
+
+
+
4. DB 감사 증적(FGA)
+
FGA 결과
+
+
+
+
+
+
+
DB 적용 상태 확인
+
아래 DB ASO 정책 동기화 표에서 백오피스 활성 컬럼과 실제 Redaction 컬럼이 일치하는지 확인합니다.
+
+ +
+
+
사용자별 실제 결과 해석
+
+
+
행 접근 권한 없음
+
VPD에서 행이 제외되므로 컬럼 마스킹 여부와 무관하게 조회 결과가 없습니다.
+
+
+
행 접근 허용 + 원문 허용 없음
+
행은 보이지만, 이 화면에서 연결한 민감 컬럼은 ASO/Data Redaction 정책에 따라 마스킹됩니다.
+
+
+
행 접근 허용 + 원문 허용 있음
+
행은 VPD 기준으로 제한되고, 원문 허용된 컬럼만 마스킹 없이 반환됩니다.
+
diff --git a/src/main/resources/templates/operation-status.html b/src/main/resources/templates/operation-status.html index 0285d1b..c3d0507 100644 --- a/src/main/resources/templates/operation-status.html +++ b/src/main/resources/templates/operation-status.html @@ -13,6 +13,48 @@
+
+
+
+ 운영 요약 +

지금 먼저 확인할 상태

+

아래 요약은 이 화면의 DB 조회 결과에서 계산한 운영자용 triage입니다. 최종 증적은 행별 상세와 접근 검증 결과로 확인합니다.

+
+ 정상 +
+
+
+
+
전체 판단
+
+ 정상
+ 현재 요약 기준으로 즉시 조치할 항목은 없습니다. +
+
+
+
행 접근 정책(VPD)
+
정상 0 · 확인 0 · 오류 0
+
+
+
컬럼 마스킹 정책(ASO)
+
적용 0 · 미적용 정상 0 · 불일치 0
+
+
+
검증 필요
+
0개 보호 객체
+
+
+
+
+
보호 객체0
+
ORDS handler 없음0
+
VPD 정책 없음0
+
ASO 불일치0
+
+
+
diff --git a/src/main/resources/templates/permissions.html b/src/main/resources/templates/permissions.html index c5d7f51..d686dc0 100644 --- a/src/main/resources/templates/permissions.html +++ b/src/main/resources/templates/permissions.html @@ -182,6 +182,47 @@

조건 코드는 토큰 context로 치환됩니다. 예를 들어 본인 담당 고객 / CUST_ID는 계약원장에서 토큰 사용자 ID의 담당 고객을 찾아 현재 객체의 CUST_ID에 적용합니다. 정적 SQL 조건식CONTRACT_STATUS = '정상'처럼 현재 객체 컬럼을 사용한 WHERE 절을 그대로 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다. 역할명은 최종 WHERE 절에 직접 들어가지 않습니다.

컬럼 원문/마스킹은 제외했습니다. 행 접근 필터는 행만 남기고, ASO/Data Redaction이 허용된 행 안에서 컬럼을 원문 또는 마스킹으로 반환합니다.

+
+ 조건 코드가 실제 VPD WHERE 조각으로 바뀌는 방식 +

이 표는 저장 규칙이 CB_AGENT_DOC_VPD_FILTER에서 어떤 predicate로 변환되는지 설명하기 위한 예시입니다. 실제 컬럼 존재 여부와 대상 객체는 DB에서 다시 검증됩니다.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
저장 조건 코드업무 의미VPD filter가 만드는 조건
ALL해당 객체의 전체 행 허용1 = 1
TOKEN_SUBJECT토큰으로 식별된 이해관계자 본인 행USER_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')
OWN_CONTRACT토큰 사용자가 담당 설계사인 계약FC_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')
CHANNEL_CONTRACT토큰 사용자의 채널에 속한 계약FC_CHANNEL = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL')
OWN_CUSTOMER / CHANNEL_CUSTOMER계약원장에서 담당자 또는 채널 기준으로 연결되는 고객·청구·담보·외부보유 행EXISTS (SELECT 1 FROM POC_2.KB_CONTRACTS ...)
STATIC_SQL현재 객체 컬럼으로 표현한 고정 조건CONTRACT_STATUS = '정상'처럼 검증된 현재 객체 컬럼 조건
+
+
diff --git a/src/main/resources/templates/probe.html b/src/main/resources/templates/probe.html index 2ce6323..79f77a0 100644 --- a/src/main/resources/templates/probe.html +++ b/src/main/resources/templates/probe.html @@ -85,9 +85,10 @@
검증 결과
    -
  • 토큰이 어떤 사용자와 역할로 해석됐는지
  • -
  • VPD 적용 후 실제로 몇 행이 보였는지
  • -
  • 예상과 다를 때 어디를 확인해야 하는지
  • +
  • 토큰이 어떤 사용자와 역할로 해석됐는지 확인합니다.
  • +
  • VPD 행 접근 정책을 지난 뒤 실제로 몇 행이 남았는지 확인합니다.
  • +
  • ASO/Data Redaction이 어떤 민감 컬럼을 마스킹했는지 확인합니다.
  • +
  • FGA 감사 증적이 있으면 DB가 실제 실행한 SQL과 VPD predicate를 확인합니다.
diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..fdbd0b1 --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-subclass