Consolidate data access control backoffice updates

This commit is contained in:
devmrko
2026-07-13 23:06:23 +09:00
parent 403298d474
commit e18b30feab
181 changed files with 11571 additions and 954 deletions

View File

@@ -0,0 +1,262 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('컬럼 마스킹')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<div class="page-title">
<h1>컬럼 마스킹</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>마스킹 방법을 사전 정의 템플릿으로 등록하고, 민감 컬럼마다 하나의 기본 규칙을 연결합니다. 컬럼 연결 목록은 <strong>마스킹 대상 블랙리스트</strong>이며, 연결하지 않은 컬럼은 ASO 마스킹 대상이 아닙니다. 행 접근은 행 접근 정책이 계속 담당하며 이 화면은 반환값 표시 방식만 다룹니다.</p>
<p>연결된 컬럼은 행 접근 정책이 조회를 허용한 사용자에게도 기본적으로 마스킹됩니다. <a href="/user-masking-rules">컬럼 원문 표시 허용 사용자</a>에 등록한 사용자만 원문을 봅니다. 이 설정은 행 접근 권한을 추가하지 않습니다. 컬럼 연결·해제 또는 연결된 규칙의 활성 상태 변경 시 DBMS_REDACT 정책도 자동 동기화됩니다.</p>
<section class="content-band mt-3 mb-0">
<h2>ASO 마스킹 적용 흐름</h2>
<p><strong>ASO/Data Redaction은 권한 테이블을 직접 조회하지 않습니다.</strong> 토큰 처리 중 <code>set_masking_rule_context(user_id)</code>가 사용자별 마스킹 예외를 세션 context로 만들고, ASO 정책은 그 context만 읽습니다.</p>
<div class="diagram-canvas">
<svg class="product-flow-diagram" viewBox="0 0 1120 410" role="img" aria-labelledby="aso-flow-title aso-flow-desc">
<title id="aso-flow-title">Bearer 토큰 기반 ASO 마스킹 적용 흐름</title>
<desc id="aso-flow-desc">Bearer 토큰으로 사용자 컨텍스트와 컬럼별 MR context를 만들고, Oracle Data Redaction이 MR context 값에 따라 원문 또는 마스킹값을 반환한다.</desc>
<defs>
<marker id="aso-flow-arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="currentColor"/></marker>
</defs>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M185 92 H225"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M385 92 H425"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M585 92 H625"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M785 92 H825"/>
<path class="diagram-link diagram-link-secondary" marker-end="url(#aso-flow-arrow)" d="M705 212 V144"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M910 136 V205"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M810 248 H850"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M930 291 V315"/>
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M850 248 H720 V315"/>
<g class="diagram-node"><rect x="25" y="48" width="160" height="88" rx="12"/><text x="105" y="79" class="diagram-node-title">Bearer Token</text><text x="105" y="106" class="diagram-node-detail">요청 주체 식별</text></g>
<g class="diagram-node"><rect x="225" y="48" width="160" height="88" rx="12"/><text x="305" y="76" class="diagram-node-title">토큰 검증</text><text x="305" y="103" class="diagram-node-detail">set_user_by_bearer</text><text x="305" y="120" class="diagram-node-note">실패 시 context 정리</text></g>
<g class="diagram-node"><rect x="425" y="48" width="160" height="88" rx="12"/><text x="505" y="76" class="diagram-node-title">사용자 context</text><text x="505" y="103" class="diagram-node-detail">CB_AGENT_CTX.USER_ID</text><text x="505" y="120" class="diagram-node-note">EMP_NO · DEPT_CODE</text></g>
<g class="diagram-node diagram-node-accent"><rect x="625" y="48" width="160" height="88" rx="12"/><text x="705" y="76" class="diagram-node-title">마스킹 context</text><text x="705" y="103" class="diagram-node-detail">set_masking_rule_</text><text x="705" y="120" class="diagram-node-detail">context(user_id)</text></g>
<g class="diagram-node diagram-node-accent"><rect x="825" y="48" width="245" height="88" rx="12"/><text x="947" y="76" class="diagram-node-title">컬럼별 세션 변수</text><text x="947" y="103" class="diagram-node-detail">MR_&lt;column_id&gt; = Y / N</text><text x="947" y="120" class="diagram-node-note">현재 DB 세션에만 설정</text></g>
<g class="diagram-node diagram-node-data"><rect x="525" y="212" width="280" height="72" rx="12"/><text x="665" y="240" class="diagram-node-title">마스킹 예외·규칙 메타데이터</text><text x="665" y="263" class="diagram-node-detail">cb_user_masking_rule · cb_column_masking_rule</text></g>
<g class="diagram-node diagram-node-accent"><rect x="850" y="205" width="160" height="86" rx="12"/><text x="930" y="235" class="diagram-node-title">DBMS_REDACT</text><text x="930" y="260" class="diagram-node-detail">SYS_CONTEXT로 MR 읽음</text></g>
<g class="diagram-node diagram-node-data"><rect x="850" y="315" width="160" height="64" rx="12"/><text x="930" y="341" class="diagram-node-title">MR = Y</text><text x="930" y="363" class="diagram-node-detail">원문 표시</text></g>
<g class="diagram-node"><rect x="640" y="315" width="160" height="64" rx="12"/><text x="720" y="341" class="diagram-node-title">MR = N / NULL</text><text x="720" y="363" class="diagram-node-detail">마스킹 적용</text></g>
</svg>
</div>
<p class="form-hint mb-0">연결 고리: 토큰 검증 후 <code>set_masking_rule_context(v_user_id)</code><code>MR_&lt;column_id&gt;</code>를 설정하고, Data Redaction 조건식은 <code>SYS_CONTEXT('CB_AGENT_CTX', 'MR_&lt;column_id&gt;')</code>만 평가합니다. 자세한 SQL은 <a href="/security-sql-scripts?script=aso-masking-runtime">ASO 마스킹 런타임 스크립트</a>에서 확인하세요.</p>
</section>
</details>
</div>
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">행 접근과 분리된 컬럼 제어</span>
<h2>ASO 마스킹 설정 순서</h2>
<p class="section-subtitle">행 접근은 행 접근 규칙과 Oracle VPD 정책이 결정합니다. 이 화면은 허용된 행 안에서 컬럼 값을 원문으로 줄지, 마스킹해서 줄지만 관리합니다.</p>
</div>
</div>
<div class="effective-preview">
<dl>
<div>
<dt>대상 컬럼 등록</dt>
<dd>마스킹 후보 블랙리스트에 DB 컬럼을 올립니다. 이 단계만으로는 값이 마스킹되지 않습니다.</dd>
</div>
<div>
<dt>기본 규칙 연결</dt>
<dd>컬럼에 활성 컬럼 마스킹 규칙을 연결하면 DBMS_REDACT 정책이 동기화됩니다.</dd>
</div>
<div>
<dt>원문 표시 허용</dt>
<dd>예외 사용자는 <a href="/user-masking-rules">컬럼 원문 표시 허용 사용자</a>에서 지정합니다.</dd>
</div>
</dl>
</div>
</section>
<section class="content-band">
<h2>DB ASO 정책 동기화</h2>
<p class="section-subtitle">이제 컬럼 기본 규칙을 연결·해제하거나 연결된 규칙을 활성·비활성화하면 DB 정책도 자동으로 반영됩니다. 아래 버튼은 자동 동기화 도입 전의 기존 설정을 한 번에 보정하거나, 운영 점검 시 현재 설정을 다시 반영할 때만 사용하세요.</p>
<form method="post" action="/masking-rules/synchronize" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<button class="btn btn-outline-primary" type="submit">현재 DB 정책 동기화</button>
</form>
<div class="table-responsive mt-3">
<table class="table table-sm align-middle">
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>레거시 VPD 컬럼 제어</th><th>DB 정책 활성</th><th>상태 판단</th></tr></thead>
<tbody>
<tr th:each="status : ${policyStatuses}">
<td><code th:text="${status.targetLabel()}">POC_2.KB_CUSTOMERS</code></td>
<td><code th:text="${status.policyName()}">KB_CUSTOMER_PII_REDACT</code></td>
<td th:text="${status.configuredColumnCount()}">0</td>
<td th:text="${status.appliedColumnCount()}">0</td>
<td><span class="badge" th:classappend="${status.legacyVpdColumnPolicyCount() == 0} ? ' text-bg-secondary' : ' text-bg-danger'" th:text="${status.legacyVpdColumnPolicyCount() == 0} ? '없음' : ${status.legacyVpdColumnPolicyCount() + '건 활성'}">없음</span></td>
<td><span class="badge" th:classappend="${status.policyEnabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${status.policyEnabled()} ? 'DB 적용 ON' : 'DB 적용 OFF'">DB 적용 OFF</span></td>
<td><span class="badge" th:classappend="${' ' + status.badgeClass()}" th:text="${status.statusLabel()}">미적용</span><br><small class="text-muted" th:text="${status.detail()}">설명</small></td>
</tr>
<tr th:if="${#lists.isEmpty(policyStatuses)}"><td colspan="7" class="text-muted">조회할 ASO 정책이 없습니다.</td></tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<h2>사전 정의 마스킹 방식</h2>
<p class="section-subtitle">임의 SQL이나 정규식을 직접 입력하지 않고 검증된 템플릿만 선택합니다.</p>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>템플릿</th><th>방식</th><th>동작</th><th>ASO 구현</th></tr></thead>
<tbody>
<tr th:each="template : ${templates}">
<td><code th:text="${template.code()}">NULLIFY</code></td>
<td th:text="${template.label()}">값 숨김(NULL)</td>
<td th:text="${template.description()}">설명</td>
<td><code th:text="${template.asoFunction()}">DBMS_REDACT.NULLIFY</code></td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<h2>컬럼 마스킹 규칙 등록</h2>
<p class="section-subtitle">업무용 이름을 붙여 템플릿을 재사용합니다. 예: <code>KB_RRN_STANDARD</code>.</p>
<form method="post" action="/masking-rules" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
규칙 코드
<input class="form-control" name="ruleCode" maxlength="64" pattern="[A-Za-z][A-Za-z0-9_]{2,63}" required placeholder="KB_RRN_STANDARD">
<span class="form-hint">영문·숫자·밑줄만 사용합니다.</span>
</label>
<label>
규칙명
<input class="form-control" name="ruleName" maxlength="100" required placeholder="주민번호 기본 마스킹">
</label>
<label>
마스킹 템플릿
<select class="form-select" name="templateCode" required>
<option th:each="template : ${templates}" th:value="${template.code()}"
th:text="${template.label()}">값 숨김(NULL)</option>
</select>
</label>
<label>
설명
<input class="form-control" name="description" maxlength="400" placeholder="적용 목적 또는 업무 기준">
</label>
<button class="btn btn-primary" type="submit">규칙 등록</button>
</form>
</section>
<section class="content-band">
<h2>등록된 컬럼 마스킹 규칙</h2>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>코드</th><th>규칙명</th><th>템플릿</th><th>설명</th><th>상태</th><th></th></tr></thead>
<tbody>
<tr th:each="rule : ${rules}">
<td><code th:text="${rule.ruleCode()}">KB_RRN_STANDARD</code></td>
<td th:text="${rule.ruleName()}">주민번호 기본 마스킹</td>
<td th:text="${rule.templateLabel()}">값 숨김(NULL)</td>
<td th:text="${rule.description() ?: '-'}">설명</td>
<td><span class="badge" th:classappend="${rule.enabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${rule.enabledYn()}">Y</span></td>
<td>
<form method="post" action="/masking-rules/active" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="ruleId" th:value="${rule.ruleId()}">
<input type="hidden" name="active" th:value="${!rule.enabled()}">
<button class="btn btn-sm btn-outline-secondary" type="submit" th:text="${rule.enabled()} ? '비활성화' : '활성화'">변경</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(rules)}"><td colspan="6" class="text-muted">등록된 컬럼 마스킹 규칙이 없습니다.</td></tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<h2>마스킹 대상 컬럼 추가</h2>
<p class="section-subtitle">DB 실제 컬럼을 ASO 마스킹 후보 블랙리스트에 등록합니다. 이 단계는 컬럼을 선택 목록에 올리는 작업이며, 실제 마스킹 적용은 아래에서 기본 규칙을 연결할 때 수행됩니다.</p>
<form method="post" action="/masking-rules/target-columns" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
대상 객체 / 컬럼
<select class="form-select" name="target" required>
<option value="">선택하세요</option>
<th:block th:each="object : ${maskingTargetObjects}">
<optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(availableMaskingColumnsByObject[object.objectId()])}">
<option th:each="columnName : ${availableMaskingColumnsByObject[object.objectId()]}"
th:value="|${object.objectId()}:${columnName}|"
th:text="${object.displayName() + '.' + columnName}">POC_2.KB_CONTRACTS.PREMIUM</option>
</optgroup>
</th:block>
</select>
<span class="form-hint">현재 관리 대상 ASO 정책이 있는 객체만 표시됩니다. 예: <code>POC_2.KB_CONTRACTS.PREMIUM</code>.</span>
</label>
<button class="btn btn-outline-primary" type="submit">대상 컬럼 추가</button>
</form>
</section>
<section class="content-band">
<h2>민감 컬럼에 기본 규칙 연결</h2>
<p class="section-subtitle">이 목록이 마스킹 대상 블랙리스트입니다. 컬럼 하나에는 활성 규칙 하나만 연결할 수 있으며, 컬럼 원문 표시 허용 사용자는 별도 화면에서 지정합니다. 오른쪽 DB 상태는 컬럼이 속한 객체의 실제 Redaction 정책 상태입니다.</p>
<form method="post" action="/masking-rules/columns" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
민감 컬럼
<select class="form-select" name="columnId" required>
<option value="">선택하세요</option>
<th:block th:each="object : ${objects}">
<optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(sensitiveColumnsByObject[object.objectId()])}">
<option th:each="column : ${sensitiveColumnsByObject[object.objectId()]}"
th:value="${column.columnId()}"
th:text="${column.columnName() + ' [' + column.displayPolicyLabel() + ']'}">RRN_MASKED</option>
</optgroup>
</th:block>
</select>
</label>
<label>
적용할 컬럼 마스킹 규칙
<select class="form-select" name="ruleId" required>
<option value="">선택하세요</option>
<option th:each="rule : ${rules}" th:if="${rule.enabled()}" th:value="${rule.ruleId()}"
th:text="${rule.ruleName() + ' · ' + rule.templateLabel()}">주민번호 기본 마스킹</option>
</select>
</label>
<button class="btn btn-primary" type="submit">컬럼에 연결</button>
</form>
<div class="table-responsive mt-3">
<table class="table table-sm align-middle">
<thead><tr><th>대상 컬럼</th><th>규칙</th><th>템플릿</th><th>백오피스 설정</th><th>DB ASO 적용 상태</th><th></th></tr></thead>
<tbody>
<tr th:each="columnRule : ${columnRules}">
<td><code th:text="${columnRule.targetLabel()}">POC_2.KB_CUSTOMERS.RRN_MASKED</code></td>
<td th:text="${columnRule.ruleName()}">주민번호 기본 마스킹</td>
<td th:text="${columnRule.template().label()}">주민등록번호 부분 마스킹</td>
<td><span class="badge" th:classappend="${columnRule.ruleEnabled()} ? ' text-bg-success' : ' text-bg-warning'" th:text="${columnRule.ruleEnabled()} ? '기본 규칙 연결됨' : '규칙 비활성'">기본 규칙 연결됨</span></td>
<td th:with="policyStatus=${policyStatusByObjectName[columnRule.objectName()]}">
<span class="badge"
th:if="${policyStatus}"
th:classappend="${' ' + policyStatus.badgeClass()}"
th:text="${policyStatus.statusLabel()}">적용됨</span>
<span class="badge text-bg-secondary" th:unless="${policyStatus}">관리 정책 없음</span>
<div class="text-muted small" th:if="${policyStatus}" th:text="${policyStatus.detail()}">상세</div>
</td>
<td>
<form method="post" action="/masking-rules/columns/delete" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="columnId" th:value="${columnRule.columnId()}">
<button class="btn btn-sm btn-outline-danger" type="submit">연결 해제</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(columnRules)}"><td colspan="6" class="text-muted">민감 컬럼에 연결된 컬럼 마스킹 규칙이 없습니다.</td></tr>
</tbody>
</table>
</div>
</section>
</main>
</body>
</html>