[UX] #577 simplify VPD product forms

This commit is contained in:
devmrko
2026-07-01 10:35:21 +09:00
parent 699909212f
commit f15b453aad
9 changed files with 128 additions and 81 deletions

View File

@@ -647,7 +647,6 @@ body {
.wizard-panel-heading {
align-items: flex-start;
display: flex;
gap: .75rem;
}
.wizard-panel-heading h3 {
@@ -1369,6 +1368,55 @@ body {
white-space: pre-wrap;
}
.permission-list-table {
min-width: 760px;
}
.permission-list-table th:nth-child(1) { width: 15%; }
.permission-list-table th:nth-child(2) { width: 19%; }
.permission-list-table th:nth-child(3) { width: 10%; }
.permission-list-table th:nth-child(4) { width: 24%; }
.permission-list-table th:nth-child(5) { width: 16%; }
.permission-list-table th:nth-child(6) { width: 16%; }
.row-management {
min-width: 10rem;
}
.row-management > summary {
color: var(--rw-primary);
cursor: pointer;
font-size: .84rem;
font-weight: 800;
}
.row-management[open] {
background: var(--rw-surface-muted);
border: 1px solid var(--rw-border);
border-radius: 8px;
padding: .65rem;
}
.operation-status-table {
table-layout: fixed;
width: 100%;
}
.operation-status-table th,
.operation-status-table td,
.operation-status-table code {
overflow-wrap: anywhere;
white-space: normal;
}
.operation-status-table th:nth-child(1) { width: 5%; }
.operation-status-table th:nth-child(2) { width: 22%; }
.operation-status-table th:nth-child(3) { width: 29%; }
.operation-status-table th:nth-child(4) { width: 20%; }
.operation-status-table th:nth-child(5) { width: 10%; }
.operation-status-table th:nth-child(6) { width: 9%; }
.operation-status-table th:nth-child(7) { width: 5%; }
.schema-summary {
display: grid;
gap: .75rem;

View File

@@ -36,17 +36,17 @@
</div>
<div class="mcp-service-grid">
<div class="mcp-service-item">
<span>1</span>
<span>연결 시작</span>
<strong><code>initialize</code></strong>
<small>기본 MCP serverInfo와 tools capability를 확인합니다.</small>
</div>
<div class="mcp-service-item">
<span>2</span>
<span>도구 목록</span>
<strong><code>tools/list</code></strong>
<small>현재 보호 객체에서 생성된 ORDS query tool과 설명/schema를 조회합니다.</small>
</div>
<div class="mcp-service-item">
<span>3</span>
<span>도구 호출</span>
<strong><code>tools/call</code></strong>
<small>tool 선택은 reasoning 이후 MCP client가 수행하므로 이 화면에서는 호출하지 않습니다.</small>
</div>

View File

@@ -14,7 +14,7 @@
<section class="content-band">
<div class="table-responsive">
<table class="table table-sm align-middle">
<table class="table table-sm align-middle operation-status-table">
<thead>
<tr>
<th>Health</th>

View File

@@ -22,11 +22,11 @@
<p class="section-description">저장 전 적용 대상과 행 범위를 검토한 뒤 VPD 권한을 반영합니다.</p>
</div>
<div class="wizard-progress" aria-label="권한 추가 단계">
<button class="wizard-step-indicator active" type="button" data-wizard-target="1">1 역할</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="2">2 객체</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="3">3 행 규칙</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="4">4 컬럼</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="5">5 검토</button>
<button class="wizard-step-indicator active" type="button" data-wizard-target="1">역할</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="2">대상</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="3">접근 범위</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="4">표시 예외</button>
<button class="wizard-step-indicator" type="button" data-wizard-target="5">검토</button>
</div>
</div>
<form method="post" action="/permissions" class="permission-wizard" data-permission-wizard>
@@ -61,7 +61,6 @@
<div class="wizard-panel active" data-wizard-step="1">
<div class="wizard-panel-heading">
<span class="wizard-step-number">1</span>
<div>
<h3>역할 선택</h3>
<p>권한을 부여하거나 거부할 역할을 선택합니다.</p>
@@ -96,7 +95,6 @@
<div class="wizard-panel" data-wizard-step="2">
<div class="wizard-panel-heading">
<span class="wizard-step-number">2</span>
<div>
<h3>보호 객체 선택</h3>
<p>등록된 보호 객체 또는 DB 스키마 객체를 선택합니다.</p>
@@ -138,7 +136,6 @@
<div class="wizard-panel" data-wizard-step="3">
<div class="wizard-panel-heading">
<span class="wizard-step-number">3</span>
<div>
<h3>권한 효과와 행 규칙</h3>
<p>허용/거부 방향과 VPD 행 필터 조건을 설정합니다.</p>
@@ -186,7 +183,6 @@
<div class="wizard-panel" data-wizard-step="4">
<div class="wizard-panel-heading">
<span class="wizard-step-number">4</span>
<div>
<h3>권한별 원문 표시 예외</h3>
<p>이 역할이 이미 볼 수 있는 행에서 마스킹을 제외할 컬럼을 여러 개 선택합니다.</p>
@@ -211,7 +207,6 @@
<div class="wizard-panel" data-wizard-step="5">
<div class="wizard-panel-heading">
<span class="wizard-step-number">5</span>
<div>
<h3>저장 전 검토</h3>
<p>저장될 권한과 예상 effective policy를 확인합니다.</p>
@@ -245,56 +240,52 @@
<section class="content-band">
<h2>접근 규칙 목록</h2>
<div class="table-responsive">
<table class="table table-sm align-middle">
<table class="table table-sm align-middle permission-list-table">
<thead>
<tr>
<th>ID</th>
<th>역할</th>
<th>테이블/뷰</th>
<th>Action</th>
<th>Effect</th>
<th>행 규칙</th>
<th>적용 필터</th>
<th>원문 표시 허용 컬럼(여러 개)</th>
<th>삭제 영향</th>
<th></th>
<th>보호 대상</th>
<th>효과</th>
<th>접근 범위</th>
<th>표시 예외</th>
<th>관리</th>
</tr>
</thead>
<tbody>
<tr th:each="permission : ${permissions}"
th:with="lastPermission=${lastPermissionByPermissionId[permission.permissionId()]}">
<td th:text="${permission.permissionId()}">100</td>
<td th:text="${permission.roleName()}">HR_DEPT_ROLE</td>
<td th:text="${permission.objectName()}">CB_V_SEARCH_DOCUMENTS</td>
<td th:text="${permission.action()}">SELECT</td>
<td>
<span class="badge"
th:classappend="${permission.permissionEffect() == 'DENY'} ? ' text-bg-danger' : ' text-bg-success'"
th:text="${permission.permissionEffect()}">ALLOW</span>
</td>
<td th:text="${permission.rules()} ?: '-'">ALL</td>
<td><pre class="table-pre" th:text="${permission.filterPreview()} ?: '-'">ALL ROWS</pre></td>
<td><pre class="table-pre" th:text="${permission.filterPreview()} ?: ${permission.rules()} ?: '-'">ALL ROWS</pre></td>
<td th:text="${permission.visibleColumns()} ?: '-'">CONTENTS</td>
<td class="delete-impact">
<span>역할이 이 TABLE/VIEW의 SELECT 권한을 잃습니다.</span>
<strong th:text="${permission.roleName() + ' -> ' + permission.objectName()}">ROLE -> OBJECT</strong>
<small th:if="${lastPermission}" class="text-danger">이 객체의 마지막 권한입니다. 삭제 후 보호 객체가 비활성화됩니다.</small>
<small th:unless="${lastPermission}">같은 객체에 다른 역할 권한이 남아 있습니다.</small>
</td>
<td>
<form method="post" action="/permissions/delete" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="permissionId" th:value="${permission.permissionId()}">
<label class="delete-confirm">
<input type="checkbox" name="confirmImpact" value="true" required>
영향 확인
</label>
<button class="btn btn-sm btn-outline-danger" type="submit">삭제</button>
</form>
<details class="row-management">
<summary>관리</summary>
<div class="delete-impact">
<span>이 역할은 이 대상의 SELECT 권한을 잃습니다.</span>
<strong th:text="${permission.roleName() + ' → ' + permission.objectName()}">ROLE → OBJECT</strong>
<small th:if="${lastPermission}" class="text-danger">마지막 권한입니다. 삭제 후 보호 대상이 비활성화될 수 있습니다.</small>
<small th:unless="${lastPermission}">같은 대상에 다른 역할 권한이 남아 있습니다.</small>
</div>
<form method="post" action="/permissions/delete" class="inline-form mt-2">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="permissionId" th:value="${permission.permissionId()}">
<label class="delete-confirm">
<input type="checkbox" name="confirmImpact" value="true" required>
영향 확인
</label>
<button class="btn btn-sm btn-outline-danger" type="submit">삭제</button>
</form>
</details>
</td>
</tr>
<tr th:if="${#lists.isEmpty(permissions)}">
<td colspan="10" class="text-muted">등록된 권한이 없습니다.</td>
<td colspan="6" class="text-muted">등록된 접근 규칙이 없습니다.</td>
</tr>
</tbody>
</table>

View File

@@ -35,7 +35,7 @@
<form hx-post="/probe" hx-target="#probe-result" hx-swap="innerHTML" class="form-grid probe-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label class="span-2">
1. 발급받은 토큰 원문
발급받은 토큰 원문
<input class="form-control" name="bearerToken" type="password" autocomplete="off"
placeholder="토큰 발급 직후 복사한 값을 붙여 넣으세요">
<span class="form-hint">원문은 DB에 저장되지 않습니다. 아래 검증 세션 사용자를 선택하면 이 입력은 무시되고 실행 중 발급·완료 즉시 폐기됩니다. 둘 다 비우면 invalid token 결과를 확인할 수 있습니다.</span>
@@ -49,7 +49,7 @@
<span class="form-hint">ORDS 호출에 사용할 10분 검증 세션을 만들고 완료 후 바로 폐기합니다.</span>
</label>
<label>
2. 확인할 데이터
확인할 데이터
<select class="form-select" name="objectId" required>
<option th:each="object : ${objects}"
th:value="${object.objectId()}"
@@ -77,18 +77,18 @@
</select>
<span class="form-hint">자료 등록 때 사용한 방식·차원과 맞춰야 합니다. AI 설정이 없으면 로컬 임베딩을 사용하세요.</span>
</label>
<button class="btn rw-btn-primary probe-submit" type="submit">3. 접근 검증 실행</button>
<button class="btn rw-btn-primary probe-submit" type="submit">접근 검증 실행</button>
</form>
</section>
<section id="probe-result" class="content-band" aria-live="polite">
<div class="empty-result-guide">
<strong>결과는 세 가지 순서로 설명합니다.</strong>
<ol>
<strong>검증 결과</strong>
<ul>
<li>토큰이 어떤 사용자와 역할로 해석됐는지</li>
<li>VPD 적용 후 실제로 몇 행이 보였는지</li>
<li>예상과 다를 때 어디를 확인해야 하는지</li>
</ol>
</ul>
</div>
</section>
</main>

View File

@@ -27,7 +27,7 @@
<section class="content-band">
<div class="section-heading">
<div>
<h2>1. 검증 사용자 선택</h2>
<h2>검증 사용자 선택</h2>
<p class="section-subtitle">토큰에는 사용자의 직접 역할과 그룹 상속 역할이 연결됩니다. 권한 자체를 토큰에 복사하지 않으므로 이후 권한 변경도 동적으로 반영됩니다.</p>
</div>
</div>
@@ -47,7 +47,7 @@
용도 메모
<input class="form-control" name="description" maxlength="200" placeholder="예: HR 권한 확인">
</label>
<button class="btn rw-btn-primary" type="submit">2. 검증 세션 발급</button>
<button class="btn rw-btn-primary" type="submit">검증 세션 발급</button>
</form>
</section>

View File

@@ -8,7 +8,7 @@
<h1>보호 상태</h1>
<details class="explanation-details">
<summary>도움말</summary>
<p>권한 화면에서 만든 사용자·그룹·역할·행·열 규칙을 Oracle VPD가 실제 TABLE/VIEW에 적용하도록 연결합니다.</p>
<p>권한 화면에서 만든 사용자·그룹·역할·행·열 규칙을 Oracle VPD가 실제 TABLE/VIEW에 적용하도록 연결합니다. Policy는 어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지 정하고, 실제 허용 조건은 권한 규칙과 Filter가 계산합니다.</p>
</details>
</div>
@@ -22,32 +22,8 @@
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">기본 동작</span>
<h2>권한체계 자동 적용</h2>
</div>
<a class="btn btn-sm rw-btn-secondary" href="/permissions">권한 규칙 확인</a>
</div>
<div class="policy-apply-flow" aria-label="VPD 적용 계층">
<span>사용자·그룹·역할</span>
<strong></strong>
<span>객체별 행·열 권한</span>
<strong></strong>
<span>동적 VPD</span>
<strong></strong>
<span>허용된 데이터만 반환</span>
</div>
<p class="text-muted mb-0"><code>CB_AGENT_DOC_VPD_FILTER</code>가 요청마다 권한체계를 읽습니다. 일반 변경은 <a href="/permissions">권한 규칙</a>에서 합니다.</p>
<details class="explanation-details mt-3">
<summary>Policy와 Filter의 역할 구분 보기</summary>
<p>Policy는 “어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지”를 정합니다. 행의 실제 허용/차단 조건은 권한 규칙과 Filter function이 계산하고, 컬럼 표시 보호는 별도 설정입니다.</p>
</details>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>보호할 수 있는 DB 객체</h2>
<p class="section-subtitle">VPD 적용 상태와 권한·검증 경로를 확인합니다.</p>
<h2>보호 대상</h2>
<p class="section-subtitle">VPD 적용과 검증 준비 상태를 확인합니다.</p>
<details class="explanation-details">
<summary>객체 상태 항목 설명 보기</summary>
<p>VPD가 붙었는지, 권한 규칙과 검증 경로가 준비됐는지 한곳에서 확인합니다. 아래 기본 적용에서는 객체만 선택하면 됩니다.</p>

View File

@@ -59,6 +59,28 @@ class GuidedFlowTemplateTest {
.doesNotContain("1 · CONTENT", "2 · ACCESS POLICY", "3 · SEARCH");
}
@Test
void productFormsUseTaskNamesInsteadOfVisibleStepNumbers() throws IOException {
String permissions = template("permissions.html");
String tokens = template("tokens.html");
String probe = template("probe.html");
String client = template("mcp-client-demo.html");
assertThat(permissions)
.contains("data-wizard-target=\"1\">역할")
.contains("data-wizard-target=\"2\">대상")
.doesNotContain(">1 역할<", ">2 객체<", "wizard-step-number");
assertThat(tokens)
.contains("검증 사용자 선택", "검증 세션 발급")
.doesNotContain("1. 검증할 사용자 선택", "2. 검증 세션 발급");
assertThat(probe)
.contains("접근 검증 실행", "<strong>검증 결과</strong>")
.doesNotContain("1. 발급받은 토큰 원문", "2. 확인할 데이터", "3. 접근 검증 실행");
assertThat(client)
.contains("연결 시작", "도구 목록", "도구 호출")
.doesNotContain("<span>1</span>", "<span>2</span>", "<span>3</span>");
}
@Test
void probeUsesOneTokenInputAndHidesTechnicalExchangeByDefault() throws IOException {
String probe = template("probe.html");