Files
vpd-permission-poc/dds-backoffice/src/main/resources/templates/dds.html

155 lines
9.3 KiB
HTML

<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('DDS 직접 조회 검증')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<header class="hero">
<span class="eyebrow">DEEP DATA SECURITY · INDEPENDENT DEMO</span>
<h1>같은 접근 결과를 다른 보안 모델로 확인합니다.</h1>
<p class="lead">이 화면은 VPD 데모와 별도로 실행되는 DDS 전용 데모입니다. 선택한 END USER로 직접 접속해 Oracle의 DATA ROLE과 DATA GRANT가 적용된 결과를 보여줍니다.</p>
<details class="explanation">
<summary>이 화면의 보안 경계</summary>
<p>이 인스턴스는 VPD 권한 테이블을 읽어 조건을 계산하지 않습니다. <code>END USER → DATA ROLE → DATA GRANT → 보호 VIEW</code>가 전부 Oracle DDS에 선언되어 있고, 조회 결과는 그 선언의 적용 결과입니다.</p>
<p class="mb-0">두 데모는 같은 접근 매트릭스를 비교하지만 실행 모델은 다릅니다. VPD는 공통 계정·Bearer·동적 predicate를 사용하고, DDS는 직접 END USER와 선언형 DATA GRANT를 사용합니다. Bearer 키를 ORDS Handler에서 사용자 이름으로 바꾸는 것만으로는 DDS 보안 컨텍스트가 만들어지지 않습니다.</p>
</details>
</header>
<section th:replace="~{fragments/layout :: architectureStrip('token')}"></section>
<section class="flow-grid" aria-label="DDS 접근 흐름">
<article class="flow-card">
<span class="flow-number">1</span>
<div><span class="eyebrow">IDENTITY</span><h2>END USER</h2><p>실제 DDS 보안 사용자로 DB에 접속합니다.</p></div>
</article>
<article class="flow-card">
<span class="flow-number">2</span>
<div><span class="eyebrow">POLICY</span><h2>DATA ROLE / GRANT</h2><p>원본별 허용 범위가 선언형으로 연결됩니다.</p></div>
</article>
<article class="flow-card">
<span class="flow-number">3</span>
<div><span class="flow-number result-number">3</span><span class="eyebrow">EVIDENCE</span><h2>조회 결과</h2><p>허용된 VIEW와 행만 반환됩니다.</p></div>
</article>
</section>
<section class="panel">
<div class="panel-heading">
<div>
<span class="eyebrow">SAME MANAGEMENT FLOW</span>
<h2>VPD 데모와 같은 권한 관리 흐름</h2>
<p>사용자·그룹·역할·데이터 권한은 같은 순서로 설계하고, 보호 적용 단계만 DDS 선언형 객체로 연결합니다.</p>
</div>
</div>
<div class="matrix-grid">
<a class="matrix-card" href="/users"><strong>1. 사용자</strong><span>누가 요청하는가</span><small>애플리케이션 사용자와 DDS END USER의 관계를 확인합니다.</small></a>
<a class="matrix-card" href="/groups"><strong>2. 그룹·역할</strong><span>어떤 업무 범위인가</span><small>그룹과 역할 상속을 같은 관리 화면에서 설정합니다.</small></a>
<a class="matrix-card" href="/permissions"><strong>3. 데이터 권한</strong><span>어떤 행·컬럼인가</span><small>행 규칙과 태그 기준을 관리 정책으로 정의합니다.</small></a>
<a class="matrix-card" href="/effective-matrix"><strong>4. 최종 권한</strong><span>DDS 적용 대상 확인</span><small>역할별 결과를 확인한 뒤 DATA ROLE/GRANT로 반영합니다.</small></a>
</div>
</section>
<div class="alert alert-warning" th:if="${configuredUserCount == 0}">
DDS 사용자 비밀번호가 설정되지 않았습니다. VM에서는 <code>DDSUSER_*_PASSWORD</code> 또는 <code>DDS_BACKOFFICE_*_PASSWORD</code> 환경 변수를 설정해야 조회할 수 있습니다.
</div>
<section class="panel">
<div class="panel-heading">
<div>
<span class="eyebrow">1 · SECURITY SUBJECT</span>
<h2>조회 주체와 데이터 원본 선택</h2>
<p>선택한 DDS END USER의 권한으로 직접 연결합니다. 비밀번호는 화면에 표시하거나 저장하지 않습니다.</p>
</div>
</div>
<form method="post" action="/dds/query" class="query-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
DDS END USER
<select class="form-select" name="userKey" required>
<option th:each="user : ${users}" th:value="${user.key()}" th:selected="${user.key() == selectedUser}"
th:text="${user.label() + ' · ' + user.username()}"></option>
</select>
<span class="field-help" th:each="user : ${users}" th:if="${user.key() == selectedUser}" th:text="${user.description()}"></span>
</label>
<label>
보호 데이터 원본
<select class="form-select" name="sourceKey" required>
<option th:each="source : ${sources}" th:value="${source.key()}" th:selected="${source.key() == selectedSource}"
th:text="${source.label()}"></option>
</select>
<span class="field-help" th:each="source : ${sources}" th:if="${source.key() == selectedSource}" th:text="${source.objectName()}"></span>
</label>
<label>
이름 검색 (선택)
<input class="form-control" name="searchText" placeholder="예: Alice">
<span class="field-help">검색 조건도 보호 VIEW 안에서만 평가됩니다.</span>
</label>
<label>
최대 행 수
<input class="form-control" name="limit" type="number" min="1" max="100" value="20">
</label>
<div class="query-submit"><button class="btn btn-primary" type="submit">DDS 권한으로 조회</button></div>
</form>
</section>
<section class="panel">
<div class="panel-heading">
<div>
<span class="eyebrow">2 · EXPECTED MATRIX</span>
<h2>원본별 권한 매트릭스</h2>
<p>VPD 데모와 같은 결과를 별도 데이터·권한 객체에서 DDS 선언형 모델로 비교합니다.</p>
</div>
</div>
<div class="matrix-grid">
<div class="matrix-card"><strong>dds_demo_my</strong><span>MY 데이터셋 허용</span><small>PG 객체는 ORA-00942</small></div>
<div class="matrix-card"><strong>dds_demo_pg</strong><span>PG 데이터셋 허용</span><small>MY 객체는 ORA-00942</small></div>
<div class="matrix-card"><strong>dds_demo_both</strong><span>PG + MY 허용</span><small>두 DATA GRANT 모두 적용</small></div>
<div class="matrix-card muted"><strong>dds_demo_none</strong><span>접속만 허용</span><small>데이터 권한 없음 · default deny</small></div>
</div>
</section>
<section class="panel" th:if="${queryResult != null}">
<div class="panel-heading result-heading">
<div>
<span class="eyebrow">3 · EVIDENCE</span>
<h2 th:text="${queryResult.userLabel() + ' · ' + queryResult.sourceLabel()}">조회 결과</h2>
<p th:text="${queryResult.objectName()}">ADMIN.V_DDS_CUSTOMERS_PG</p>
</div>
<span class="result-status" th:classappend="${queryResult.success()} ? ' success' : ' blocked'"
th:text="${queryResult.success()} ? '정책 적용됨' : '접근 차단'">상태</span>
</div>
<div class="alert" th:classappend="${queryResult.success()} ? ' alert-success' : ' alert-warning'">
<strong th:text="${queryResult.title()}">결과</strong>
<span th:text="${queryResult.message()}">메시지</span>
<span th:if="${queryResult.oracleCode() != null}" class="technical-code" th:text="${'Oracle code: ' + queryResult.oracleCode()}"></span>
</div>
<div class="context-grid" th:if="${queryResult.success()}">
<div><span>SESSION_USER</span><strong th:text="${queryResult.sessionUser() ?: '-'}">-</strong></div>
<div><span>ORA_END_USER_CONTEXT</span><strong th:text="${queryResult.endUser() ?: '-'}">-</strong></div>
<div><span>반환 행</span><strong th:text="${#lists.size(queryResult.rows())}">0</strong></div>
</div>
<div class="table-responsive" th:if="${queryResult.success() and queryResult.hasRows()}">
<table class="table align-middle">
<thead><tr><th>ID</th><th>이름</th><th>이메일</th><th>가입일</th><th>지역</th></tr></thead>
<tbody>
<tr th:each="row : ${queryResult.rows()}">
<td th:text="${row.customerId()}">1</td>
<td th:text="${row.fullName()}">Alice</td>
<td th:text="${row.email() ?: 'NULL'}">alice@example.com</td>
<td th:text="${row.signupDate() ?: '-'}">2026-01-01</td>
<td th:text="${row.region()}">APAC</td>
</tr>
</tbody>
</table>
</div>
<div class="empty-result" th:if="${queryResult.success() and !queryResult.hasRows()}">조회 조건을 통과한 행이 없습니다.</div>
<details class="explanation">
<summary>판정 근거와 운영 메모</summary>
<p>성공한 경우 연결 시점의 <code>SESSION_USER</code><code>ORA_END_USER_CONTEXT</code>를 함께 확인할 수 있습니다. 차단된 경우 DDS가 보호 객체 자체를 숨겨 <code>ORA-00942</code>를 반환하는 것이 정상입니다.</p>
<p class="mb-0">현재 앱은 결과만 읽습니다. 권한 변경은 <code>CREATE DATA ROLE</code>, <code>GRANT DATA ROLE</code>, <code>CREATE DATA GRANT</code> 승인 절차를 거쳐 SQL로 관리합니다.</p>
</details>
</section>
</main>
</body>
</html>