feat: add dedicated DDS permission backoffice instance
This commit is contained in:
151
dds-backoffice/src/main/resources/templates/dds.html
Normal file
151
dds-backoffice/src/main/resources/templates/dds.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Deep Data Security 접근 관리</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/css/dds.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="dds-nav">
|
||||
<div class="container d-flex align-items-center gap-3">
|
||||
<a class="brand" href="/">DDS Permission Console</a>
|
||||
<span class="track-badge">별도 인스턴스 · :8083</span>
|
||||
<form method="post" action="/logout" class="ms-auto">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button class="btn btn-sm btn-outline-light" type="submit">로그아웃</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-4">
|
||||
<header class="hero">
|
||||
<span class="eyebrow">DEEP DATA SECURITY · DIRECT END USER</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">Bearer 키를 ORDS Handler에서 사용자 이름으로 바꾸는 것만으로는 DDS 보안 컨텍스트가 만들어지지 않습니다. 그래서 이 비교 트랙은 직접 END USER logon을 검증합니다.</p>
|
||||
</details>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
|
||||
<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와 같은 4가지 업무 사례를 DDS의 선언형 권한으로 재현합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="matrix-grid">
|
||||
<div class="matrix-card"><strong>ddsuser_my</strong><span>MY 원본 허용</span><small>PG 객체는 ORA-00942</small></div>
|
||||
<div class="matrix-card"><strong>ddsuser_pg</strong><span>PG 원본 허용</span><small>MY 객체는 ORA-00942</small></div>
|
||||
<div class="matrix-card"><strong>ddsuser_both</strong><span>PG + MY 허용</span><small>두 DATA GRANT 모두 적용</small></div>
|
||||
<div class="matrix-card muted"><strong>ddsuser_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>
|
||||
Reference in New Issue
Block a user