[UX] #577 clarify VPD flow and SQL evidence
This commit is contained in:
@@ -148,6 +148,8 @@
|
||||
|
||||
Oracle은 VPD의 내부 rewrite 결과를 별도 최종 SQL 문자열로 `V$SQL`에 보관하지 않는다. 따라서 원문 SQL과 실행계획 predicate를 함께 제시하는 것이 실제 실행에 대한 DB 증적이다. `executions`, `rows_processed`, `elapsed_time`, `buffer_gets`는 cursor 누적값이며 단일 HTTP 요청만의 계측값은 아니다.
|
||||
|
||||
화면에서는 설명용 표현을 **실행 요청 SQL (재현)**, cursor에서 읽은 원문을 **실제 DB cursor SQL**로 구분한다. `V$SQL` 권한이 없는 환경에서도 재현 SQL은 표시하고, 실제 cursor 증적이 없다는 이유를 별도로 표시한다.
|
||||
|
||||
최근 SQL 매칭은 대상 객체와 최근 2분 실행 시각으로 고르므로, 같은 객체를 동시에 호출하는 운영 환경에서는 다른 요청 cursor가 선택될 가능성이 있다. 현재 UI는 이를 “최근 실행 증적”으로 명시한다. 요청별 완전 상관이 필요해지면 ORDS Handler에 검증 요청 ID를 주입해 고유 SQL comment/module-action으로 cursor를 추적하는 후속 작업으로 확장한다.
|
||||
|
||||
실행 증적 연결에는 최소한 `V$SQL`과 `DBMS_XPLAN.DISPLAY_CURSOR`를 조회할 수 있는 catalog 권한이 필요하다. 해당 권한은 ORDS parsing schema 연결에서 먼저 조회하고, 사용할 수 없으면 백오피스 연결로 한 번 더 시도한다. Autonomous의 일반 `ADMIN` 계정은 `SYS.V_$SQL` 권한을 다른 계정에 위임하지 못할 수 있으므로, 이 경우에는 DBA가 [34_agent_ords_execution_evidence_grant.sql](../../sql/adb/34_agent_ords_execution_evidence_grant.sql)을 실행해야 한다. cursor 미발견과 catalog 권한 미보유는 UI에서 서로 다른 안내로 표시하며, 어느 경우도 권한 검증 결과를 실패시키지 않는다.
|
||||
|
||||
@@ -104,7 +104,7 @@ UX-1부터 시작한다. 기존 데이터 모델과 VPD 로직은 바꾸지 않
|
||||
| 별도 Filter 관리 | 고급 접근 조건 | 예외 정책 기능 |
|
||||
| 운영 상태 / 연결 설정 | 운영 현황 / 시스템 설정 | 운영성·연동 설정 기능 |
|
||||
|
||||
상단 메뉴는 VPD 업무 범위를 빠짐없이 드러내는 `권한 관리 · 보호·검증 · 연동 도구 · 운영·고급`으로 구성한다. 각 영역은 사용자·역할·접근 규칙, 보호 상태·검증 세션, 조회 연동·지식 검색·MCP, 운영 현황·고급 접근 조건·시스템 설정을 각각 포함한다. DDS와 공통으로 가져가는 것은 메뉴 분류가 아니라 **두 줄 탐색 방식**이다. 첫 줄에는 제품명·1차 메뉴·로그아웃을 함께 두고, 1차 메뉴를 누르면 해당 하위 메뉴가 팝업으로 뜨지 않고 바로 아래 전폭 줄에 표시된다. 현재 페이지가 속한 메뉴는 처음부터 아래 줄을 열어 둔다. 절차나 구현 방식이 아니라 사용자가 하려는 일을 메뉴명으로 쓴다. 메뉴와 기본 화면, wizard에도 `1.`, `2.`, 단계 레일, `01/02` 배지를 두지 않는다. 선택된 입력 영역은 역할·대상·접근 범위처럼 의미 있는 이름으로만 표시한다.
|
||||
상단 메뉴는 VPD 업무 범위를 빠짐없이 드러내는 `권한 관리 · 보호·검증 · 연동 도구 · 운영`으로 구성한다. 각 영역은 사용자·역할·접근 규칙, 보호 상태·검증 세션, 조회 연동·지식 검색·MCP, 운영 현황을 각각 포함한다. 시스템 연결, DB 준비, 고급 접근 조건은 일반 업무 메뉴와 섞지 않고 오른쪽 **관리자** 유틸리티로 분리한다. DDS와 공통으로 가져가는 것은 메뉴 분류가 아니라 **두 줄 탐색 방식**이다. 첫 줄에는 제품명·1차 메뉴·관리자·로그아웃을 함께 두고, 1차 메뉴를 누르면 해당 하위 메뉴가 팝업으로 뜨지 않고 바로 아래 전폭 줄에 표시된다. 현재 페이지가 속한 메뉴는 처음부터 아래 줄을 열어 둔다. 절차나 구현 방식이 아니라 사용자가 하려는 일을 메뉴명으로 쓴다. 메뉴와 기본 화면, wizard에도 `1.`, `2.`, 단계 레일, `01/02` 배지를 두지 않는다. 선택된 입력 영역은 역할·대상·접근 범위처럼 의미 있는 이름으로만 표시한다.
|
||||
|
||||
기본 페이지는 제목과 현재 설정·주요 행동만 노출한다. VPD/ORDS 처리 원리, 배경 설명, 주의사항은 상단의 공통 `도움말` 버튼을 눌렀을 때만 펼친다. 이 규칙으로 제품 화면이 관리 매뉴얼처럼 보이는 문제를 줄이고, 설정 자체를 전면에 둔다.
|
||||
|
||||
@@ -117,3 +117,12 @@ UX-1부터 시작한다. 기존 데이터 모델과 VPD 로직은 바꾸지 않
|
||||
- 접근 규칙 목록은 ID·고정 ACTION·삭제 경고를 기본 열에서 빼고, 필요한 삭제 영향만 행의 `관리` 상세에서 확인하게 한다.
|
||||
- 보호 상태의 동작 설명은 상단 도움말로 옮기고, 기본 화면은 보호 대상과 적용 작업부터 보이게 한다.
|
||||
- 운영 현황은 긴 ORDS·VPD 값 때문에 오른쪽 열이 밀리지 않도록 고정 열 폭과 줄바꿈을 적용한다.
|
||||
|
||||
## 10. 홈·도움말 정보 구조 보완 — 2026-07-01
|
||||
|
||||
첫 화면은 설명을 길게 나열하는 대시보드가 아니라, `권한 관리 → 보호·검증 → 연동 도구 → 운영`의 큰 업무 흐름과 바로 시작할 행동, 현재 상태를 보여준다. 메뉴의 상세 역할과 권한 처리 원리는 홈의 도움말에서만 확장한다.
|
||||
|
||||
- 도움말은 각 메뉴의 책임을 짧게 설명하고, 운영자가 길을 잃지 않도록 관리자 기능의 범위를 별도로 명시한다.
|
||||
- 권한 적용 다이어그램은 하나의 VPD/DDS 실행 계정(`CB_ORDS`)이 요청 토큰으로 실제 업무 사용자(`CB_APP_USER`)를 식별한 뒤 `CB_AGENT_CTX`를 설정하고, 역할·권한 테이블을 읽어 VPD 조건을 계산하는 구조를 보인다.
|
||||
- ERD는 직접 역할(`CB_USER_ROLE`)과 그룹 상속(`CB_USER_GROUP` → `CB_GROUP_ROLE`)이 같은 역할·권한·규칙 체계로 합쳐지는 점을 보여준다. 이 구조 때문에 실행 DB 계정을 사용자마다 만들지 않아도 요청별로 다른 데이터 범위가 적용된다.
|
||||
- 접근 검증에는 읽기용 **실행 요청 SQL**과 실제 DB cursor 증적을 구분한다. `V$SQL`/`DBMS_XPLAN` 권한이 있으면 원문 SQL·SQL_ID·Predicate Information을 보여주며, 없으면 재현 SQL은 보여주되 증적 권한이 없다는 상태를 명확히 표시한다. Oracle VPD의 내부 최종 rewrite 문자열은 별도 SQL 텍스트로 보관되지 않는다.
|
||||
|
||||
@@ -123,6 +123,31 @@ body {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.rw-nav-utility {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.rw-admin-trigger {
|
||||
background: var(--rw-surface);
|
||||
border: 1px solid var(--rw-border-strong);
|
||||
border-radius: 999px;
|
||||
color: var(--rw-text);
|
||||
font-size: .82rem;
|
||||
font-weight: 750;
|
||||
min-height: 2rem;
|
||||
padding: .35rem .7rem;
|
||||
}
|
||||
|
||||
.rw-admin-trigger:hover,
|
||||
.rw-admin-trigger:focus,
|
||||
.rw-admin-trigger.active {
|
||||
background: var(--rw-primary-soft);
|
||||
border-color: var(--rw-primary);
|
||||
color: var(--rw-primary-hover);
|
||||
}
|
||||
|
||||
.rw-menu-trigger {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
@@ -1880,6 +1905,200 @@ body {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.product-help-overview {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.product-help-section + .product-help-section {
|
||||
border-top: 1px solid var(--rw-border);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.product-help-section h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.product-help-section p {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.menu-role-grid {
|
||||
display: grid;
|
||||
gap: .65rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin: .75rem 0 0;
|
||||
}
|
||||
|
||||
.menu-role-grid > div {
|
||||
background: var(--rw-surface);
|
||||
border: 1px solid var(--rw-border);
|
||||
border-radius: 8px;
|
||||
padding: .75rem;
|
||||
}
|
||||
|
||||
.menu-role-grid dt {
|
||||
color: var(--rw-text);
|
||||
font-size: .88rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.menu-role-grid dd {
|
||||
color: var(--rw-muted);
|
||||
font-size: .82rem;
|
||||
line-height: 1.45;
|
||||
margin: .3rem 0 0;
|
||||
}
|
||||
|
||||
.diagram-canvas {
|
||||
background: var(--rw-surface);
|
||||
border: 1px solid var(--rw-border);
|
||||
border-radius: 8px;
|
||||
margin-top: .75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.product-flow-diagram,
|
||||
.permission-erd-diagram {
|
||||
display: block;
|
||||
min-width: 900px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.diagram-link {
|
||||
fill: none;
|
||||
stroke: var(--rw-primary);
|
||||
stroke-width: 2.25;
|
||||
}
|
||||
|
||||
.diagram-link-secondary {
|
||||
stroke: var(--rw-border-strong);
|
||||
stroke-dasharray: 5 4;
|
||||
}
|
||||
|
||||
#flow-arrow path,
|
||||
#erd-arrow path {
|
||||
fill: var(--rw-primary);
|
||||
}
|
||||
|
||||
.diagram-node rect,
|
||||
.erd-entity rect {
|
||||
fill: var(--rw-surface-muted);
|
||||
stroke: var(--rw-border-strong);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.diagram-node-accent rect,
|
||||
.erd-entity-accent rect {
|
||||
fill: var(--rw-primary-soft);
|
||||
stroke: var(--rw-primary);
|
||||
}
|
||||
|
||||
.diagram-node-data rect,
|
||||
.erd-entity-data rect {
|
||||
fill: #e7eee9;
|
||||
stroke: var(--rw-success);
|
||||
}
|
||||
|
||||
.diagram-node text,
|
||||
.erd-entity text {
|
||||
fill: var(--rw-text);
|
||||
font-family: "Oracle Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.diagram-node-title,
|
||||
.erd-entity text:first-of-type {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.diagram-node-detail,
|
||||
.erd-entity text + text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.diagram-node-note {
|
||||
fill: var(--rw-muted) !important;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.product-help-sql {
|
||||
background: var(--rw-primary-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--rw-primary) 26%, var(--rw-border));
|
||||
border-radius: 8px;
|
||||
padding: .85rem;
|
||||
}
|
||||
|
||||
.dashboard-menu-flow {
|
||||
background: var(--rw-surface);
|
||||
border: 1px solid var(--rw-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--rw-shadow);
|
||||
margin-bottom: 1.25rem;
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
.dashboard-section-heading h2 {
|
||||
font-size: 1.12rem;
|
||||
font-weight: 800;
|
||||
margin: .25rem 0 0;
|
||||
}
|
||||
|
||||
.dashboard-flow-grid {
|
||||
display: grid;
|
||||
gap: .9rem;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin-top: .9rem;
|
||||
}
|
||||
|
||||
.dashboard-flow-card {
|
||||
background: var(--rw-surface-muted);
|
||||
border: 1px solid var(--rw-border);
|
||||
border-radius: 8px;
|
||||
color: var(--rw-text);
|
||||
min-height: 6.4rem;
|
||||
padding: .9rem;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dashboard-flow-card:hover,
|
||||
.dashboard-flow-card:focus {
|
||||
border-color: var(--rw-primary);
|
||||
color: var(--rw-text);
|
||||
}
|
||||
|
||||
.dashboard-flow-card strong,
|
||||
.dashboard-flow-card span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dashboard-flow-card strong {
|
||||
font-size: .95rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dashboard-flow-card span {
|
||||
color: var(--rw-muted);
|
||||
font-size: .8rem;
|
||||
line-height: 1.4;
|
||||
margin-top: .3rem;
|
||||
}
|
||||
|
||||
.dashboard-flow-card:not(:last-child)::after {
|
||||
color: var(--rw-primary);
|
||||
content: "→";
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
position: absolute;
|
||||
right: -.72rem;
|
||||
top: calc(50% - .7rem);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dashboard-command-center {
|
||||
background: var(--rw-surface);
|
||||
border: 1px solid var(--rw-border);
|
||||
@@ -2353,6 +2572,12 @@ body {
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.rw-nav-utility {
|
||||
justify-content: flex-start;
|
||||
margin-left: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-hero,
|
||||
.dashboard-command-center {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -2371,12 +2596,25 @@ body {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.journey-grid,
|
||||
.dashboard-flow-grid,
|
||||
.menu-role-grid,
|
||||
.macro-micro-grid,
|
||||
.default-vpd-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.journey-grid,
|
||||
.macro-micro-grid,
|
||||
.default-vpd-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-flow-card:nth-child(2)::after,
|
||||
.dashboard-flow-card::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.guided-check-intro {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
@@ -2394,7 +2632,9 @@ body {
|
||||
}
|
||||
|
||||
.dashboard-action-grid,
|
||||
.dashboard-state dl {
|
||||
.dashboard-state dl,
|
||||
.dashboard-flow-grid,
|
||||
.menu-role-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,83 @@
|
||||
<header class="dashboard-hero">
|
||||
<div>
|
||||
<span class="architecture-kicker">VPD 권한 운영</span>
|
||||
<h1>권한 관리</h1>
|
||||
<h1>권한 운영 흐름</h1>
|
||||
<p>업무 사용자와 데이터 접근 기준을 관리하고, DB가 적용한 결과까지 확인합니다.</p>
|
||||
</div>
|
||||
<div class="dashboard-hero-actions" aria-label="주요 작업">
|
||||
<a class="btn rw-btn-primary" href="/probe">접근 검증</a>
|
||||
<a class="btn rw-btn-secondary" href="/permissions">접근 규칙</a>
|
||||
</div>
|
||||
<details class="explanation-details dashboard-explanation">
|
||||
<summary>도움말</summary>
|
||||
<p>사용자·그룹·역할에 권한을 연결하면 Oracle VPD가 요청할 때마다 그 규칙을 읽어 허용된 행만 반환합니다. 별도 SQL 필터를 만드는 일은 예외적인 고급 작업입니다.</p>
|
||||
<summary>도움말: 메뉴와 권한 적용 구조 보기</summary>
|
||||
<div class="product-help-overview">
|
||||
<section class="product-help-section">
|
||||
<h2>왜 권한 테이블을 따로 관리하나요?</h2>
|
||||
<p>DB 연결은 VPD/DDS 실행 계정 하나로 유지하되, 실제 업무 사용자는 <code>CB_APP_USER</code>와 역할·권한 테이블에서 찾습니다. 요청마다 토큰이 사용자를 식별하고 VPD가 그 사용자의 권한만 조건으로 계산하므로, 같은 실행 계정으로도 사용자마다 다른 행과 컬럼을 안전하게 반환할 수 있습니다.</p>
|
||||
</section>
|
||||
|
||||
<section class="product-help-section">
|
||||
<h2>메뉴 안내</h2>
|
||||
<dl class="menu-role-grid">
|
||||
<div><dt>권한 관리</dt><dd>사용자·그룹·역할과 접근 규칙을 정하고, 최종 권한을 확인합니다.</dd></div>
|
||||
<div><dt>보호·검증</dt><dd>보호 정책을 연결하고 토큰으로 실제 DB 조회 결과를 검증합니다.</dd></div>
|
||||
<div><dt>연동 도구</dt><dd>조회 대상, ORDS 연동, 지식 검색과 MCP 연결을 관리합니다.</dd></div>
|
||||
<div><dt>운영</dt><dd>현재 연결과 실행 상태를 확인합니다.</dd></div>
|
||||
<div><dt>관리자</dt><dd>일상 권한 작업과 분리된 시스템 연결, DB 준비, 고급 예외 조건을 관리합니다.</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="product-help-section">
|
||||
<h2>요청마다 권한이 적용되는 흐름</h2>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="product-flow-diagram" viewBox="0 0 1120 270" role="img" aria-labelledby="vpd-flow-title vpd-flow-desc">
|
||||
<title id="vpd-flow-title">VPD 권한 적용 흐름</title>
|
||||
<desc id="vpd-flow-desc">Bearer Token 요청이 공용 실행 계정과 사용자 컨텍스트를 거쳐 권한 테이블에서 계산한 VPD 조건으로 보호 데이터를 조회하는 흐름</desc>
|
||||
<defs><marker id="flow-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M190 92 H245"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M395 92 H450"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M600 92 H655"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M805 92 H860"/>
|
||||
<path class="diagram-link diagram-link-secondary" marker-end="url(#flow-arrow)" d="M720 210 V143"/>
|
||||
<g class="diagram-node"><rect x="30" y="48" width="160" height="88" rx="12"/><text x="110" y="81" class="diagram-node-title">요청·토큰</text><text x="110" y="108" class="diagram-node-detail">Bearer Token</text></g>
|
||||
<g class="diagram-node"><rect x="245" y="48" width="150" height="88" rx="12"/><text x="320" y="78" class="diagram-node-title">공용 실행 계정</text><text x="320" y="105" class="diagram-node-detail">CB_ORDS</text><text x="320" y="123" class="diagram-node-note">한 개의 DB 연결</text></g>
|
||||
<g class="diagram-node"><rect x="450" y="48" width="150" height="88" rx="12"/><text x="525" y="78" class="diagram-node-title">사용자 컨텍스트</text><text x="525" y="105" class="diagram-node-detail">CB_AGENT_CTX</text><text x="525" y="123" class="diagram-node-note">사용자별로 설정</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="655" y="48" width="150" height="88" rx="12"/><text x="730" y="78" class="diagram-node-title">VPD 조건 계산</text><text x="730" y="105" class="diagram-node-detail">ALLOW · DENY · 행 · 열</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="860" y="48" width="210" height="88" rx="12"/><text x="965" y="78" class="diagram-node-title">보호 데이터 조회</text><text x="965" y="105" class="diagram-node-detail">허용된 행·컬럼만 반환</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="570" y="180" width="300" height="62" rx="12"/><text x="720" y="207" class="diagram-node-title">사용자 · 역할 · 권한 테이블</text><text x="720" y="229" class="diagram-node-detail">요청 시 동적으로 조회</text></g>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="product-help-section">
|
||||
<h2>권한 데이터 모델</h2>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="permission-erd-diagram" viewBox="0 0 1180 355" role="img" aria-labelledby="erd-title erd-desc">
|
||||
<title id="erd-title">VPD 권한 ERD</title>
|
||||
<desc id="erd-desc">사용자와 그룹에서 역할을 얻고 역할에서 권한과 권한 규칙을 거쳐 보호 대상으로 연결되는 데이터 모델</desc>
|
||||
<defs><marker id="erd-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>
|
||||
<path class="diagram-link" marker-end="url(#erd-arrow)" d="M175 90 H230"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M390 90 H445"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M605 90 H660"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M820 90 H875"/>
|
||||
<path class="diagram-link diagram-link-secondary" marker-end="url(#erd-arrow)" d="M175 245 H230"/><path class="diagram-link diagram-link-secondary" marker-end="url(#erd-arrow)" d="M390 245 H445"/><path class="diagram-link diagram-link-secondary" marker-end="url(#erd-arrow)" d="M605 245 V144"/>
|
||||
<path class="diagram-link diagram-link-secondary" marker-end="url(#erd-arrow)" d="M100 290 V144"/>
|
||||
<g class="erd-entity"><rect x="25" y="50" width="150" height="80" rx="10"/><text x="100" y="80">CB_APP_USER</text><text x="100" y="105">업무 사용자</text></g>
|
||||
<g class="erd-entity"><rect x="230" y="50" width="160" height="80" rx="10"/><text x="310" y="80">CB_USER_ROLE</text><text x="310" y="105">직접 역할 연결</text></g>
|
||||
<g class="erd-entity"><rect x="445" y="50" width="160" height="80" rx="10"/><text x="525" y="80">CB_APP_ROLE</text><text x="525" y="105">역할</text></g>
|
||||
<g class="erd-entity"><rect x="660" y="50" width="160" height="80" rx="10"/><text x="740" y="80">CB_PERMISSION</text><text x="740" y="105">객체 접근</text></g>
|
||||
<g class="erd-entity"><rect x="875" y="50" width="160" height="80" rx="10"/><text x="955" y="80">CB_PERMISSION_RULE</text><text x="955" y="105">행·열 조건</text></g>
|
||||
<g class="erd-entity erd-entity-accent"><rect x="1045" y="50" width="115" height="80" rx="10"/><text x="1102" y="80">보호 대상</text><text x="1102" y="105">TABLE / VIEW</text></g>
|
||||
<g class="erd-entity"><rect x="25" y="205" width="150" height="80" rx="10"/><text x="100" y="235">CB_USER_GROUP</text><text x="100" y="260">그룹 소속</text></g>
|
||||
<g class="erd-entity"><rect x="230" y="205" width="160" height="80" rx="10"/><text x="310" y="235">CB_GROUP</text><text x="310" y="260">사용자 그룹</text></g>
|
||||
<g class="erd-entity"><rect x="445" y="205" width="160" height="80" rx="10"/><text x="525" y="235">CB_GROUP_ROLE</text><text x="525" y="260">그룹 역할 연결</text></g>
|
||||
<g class="erd-entity erd-entity-data"><rect x="745" y="205" width="215" height="80" rx="10"/><text x="852" y="235">CB_AGENT_BEARER_KEY</text><text x="852" y="260">토큰 → 사용자 식별</text></g>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="product-help-section product-help-sql">
|
||||
<h2>실행 SQL은 어디에서 확인하나요?</h2>
|
||||
<p><strong>실행 요청 SQL</strong>은 현재 사용자 컨텍스트와 VPD 조건을 결합한 읽기용 재현 SQL입니다. 실제 DB cursor가 기록한 원문 SQL과 Predicate Information은 <code>V$SQL</code>·<code>DBMS_XPLAN</code> 권한이 있을 때 접근 검증 결과에서 함께 표시됩니다. Oracle은 VPD가 내부적으로 붙인 최종 rewrite 문자열 자체를 별도 SQL 텍스트로 보관하지 않으므로, 실제 적용 근거는 실행계획의 Predicate Information으로 확인합니다.</p>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
</header>
|
||||
|
||||
@@ -25,9 +93,22 @@
|
||||
<div class="mt-2" th:if="${showSupportCommand}"><code>./run.sh backoffice-support</code></div>
|
||||
</div>
|
||||
|
||||
<section class="dashboard-menu-flow" aria-labelledby="menu-flow-title">
|
||||
<div class="dashboard-section-heading">
|
||||
<span class="architecture-kicker">업무 흐름</span>
|
||||
<h2 id="menu-flow-title">설정은 권한 관리에서, 적용 확인은 접근 검증에서</h2>
|
||||
</div>
|
||||
<div class="dashboard-flow-grid">
|
||||
<a class="dashboard-flow-card" href="/permissions"><strong>권한 관리</strong><span>사용자·역할·접근 규칙</span></a>
|
||||
<a class="dashboard-flow-card" href="/vpd-policies"><strong>보호·검증</strong><span>보호 연결·검증 세션·접근 확인</span></a>
|
||||
<a class="dashboard-flow-card" href="/objects"><strong>연동 도구</strong><span>조회 대상·지식 검색·MCP 연동</span></a>
|
||||
<a class="dashboard-flow-card" href="/operation-status"><strong>운영</strong><span>상태와 실행 결과 확인</span></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-command-center" aria-label="주요 권한 작업">
|
||||
<div class="dashboard-primary-action">
|
||||
<h2>주요 작업</h2>
|
||||
<h2>바로 시작</h2>
|
||||
<div class="dashboard-action-grid">
|
||||
<a class="dashboard-action" href="/permissions">
|
||||
<span><strong>접근 규칙</strong><small>역할별 객체·행·컬럼 접근을 설정합니다.</small></span>
|
||||
@@ -51,21 +132,8 @@
|
||||
</section>
|
||||
|
||||
<details class="dashboard-details">
|
||||
<summary>상세 현황</summary>
|
||||
<summary>등록 대상 상세</summary>
|
||||
<div class="dashboard-details-body">
|
||||
<section class="macro-micro-grid" aria-label="권한 적용 방식">
|
||||
<div>
|
||||
<span class="architecture-kicker">전체 관점</span>
|
||||
<h2>권한체계가 유일한 기준입니다.</h2>
|
||||
<p>일상적인 변경은 사용자, 그룹, 역할, 권한 규칙에서만 합니다. 같은 규칙을 화면과 DB 필터에 이중으로 작성하지 않습니다.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="architecture-kicker">실행 관점</span>
|
||||
<h2>VPD가 요청마다 조건을 계산합니다.</h2>
|
||||
<p>토큰에서 사용자를 찾고 직접 역할과 그룹 상속 역할을 합친 뒤, 객체의 ALLOW/DENY 및 행·열 규칙을 적용합니다. 근거는 결과 확인 단계에서 봅니다.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-object-list">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
|
||||
@@ -18,13 +18,16 @@
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="access" aria-controls="submenu-access" aria-expanded="false">권한 관리</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="protection" aria-controls="submenu-protection" aria-expanded="false">보호·검증</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="integration" aria-controls="submenu-integration" aria-expanded="false">연동 도구</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="operations" aria-controls="submenu-operations" aria-expanded="false">운영·고급</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="operations" aria-controls="submenu-operations" aria-expanded="false">운영</button>
|
||||
</div>
|
||||
<form method="post" action="/logout" class="ms-auto">
|
||||
<div class="rw-nav-utility ms-auto">
|
||||
<button class="rw-admin-trigger" type="button" data-submenu-trigger="admin" aria-controls="submenu-admin" aria-expanded="false">관리자</button>
|
||||
<form method="post" action="/logout">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">로그아웃</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rw-submenu-bar" data-submenu-bar hidden>
|
||||
<div class="container">
|
||||
<div id="submenu-access" class="rw-submenu" data-submenu-panel="access" role="navigation" aria-label="권한 관리 메뉴" hidden>
|
||||
@@ -48,10 +51,13 @@
|
||||
<a class="nav-link" href="/mcp-sse">MCP 서비스</a>
|
||||
<a class="nav-link" href="/mcp-client-demo">연동 점검</a>
|
||||
</div>
|
||||
<div id="submenu-operations" class="rw-submenu" data-submenu-panel="operations" role="navigation" aria-label="운영 및 고급 메뉴" hidden>
|
||||
<div id="submenu-operations" class="rw-submenu" data-submenu-panel="operations" role="navigation" aria-label="운영 메뉴" hidden>
|
||||
<a class="nav-link" href="/operation-status">운영 현황</a>
|
||||
</div>
|
||||
<div id="submenu-admin" class="rw-submenu" data-submenu-panel="admin" role="navigation" aria-label="관리자 메뉴" hidden>
|
||||
<a class="nav-link" href="/vpd-filter-policies">고급 접근 조건</a>
|
||||
<a class="nav-link" href="/settings">시스템 설정</a>
|
||||
<a class="nav-link" href="/settings/database">DB 준비 상태</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,18 +142,13 @@
|
||||
th:text="${result.executionEvidenceMessage()}"></div>
|
||||
</section>
|
||||
|
||||
<div class="alert alert-light mb-0" th:if="${result.successLike() and !result.hasExecutionEvidence() and result.executionEvidenceMessage() != null}"
|
||||
th:text="${result.executionEvidenceMessage()}">
|
||||
최근 SQL_ID를 찾지 못했습니다.
|
||||
</div>
|
||||
|
||||
<section class="result-section sql-trace-section" th:if="${result.hasSqlTrace()}">
|
||||
<div class="section-heading compact-heading">
|
||||
<div>
|
||||
<h3>권한 조건 재현 SQL</h3>
|
||||
<h3>실행 요청 SQL</h3>
|
||||
<p class="section-subtitle"
|
||||
th:text="${vectorSearch ? 'VECTOR_DISTANCE 유사도 검색과 현재 사용자의 역할 기반 VPD 권한 필터를 결합한 설명용 SQL입니다.' : 'ORDS Handler가 반환한 VPD predicate 또는 같은 토큰 컨텍스트를 기본 조회문에 결합한 설명용 SQL입니다.'}">
|
||||
ORDS Handler가 반환한 VPD predicate 또는 같은 토큰 컨텍스트를 재현해 조회한 predicate를 기본 조회문에 합친 형태입니다.
|
||||
th:text="${vectorSearch ? 'VECTOR_DISTANCE 검색과 현재 사용자의 역할 기반 VPD 조건을 결합한 읽기용 SQL입니다.' : '현재 사용자 컨텍스트에서 계산한 VPD 조건을 ORDS 조회문에 결합한 읽기용 SQL입니다.'}">
|
||||
현재 사용자 컨텍스트에서 계산한 VPD 조건을 ORDS 조회문에 결합한 읽기용 SQL입니다.
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge text-bg-light">DBMS_RLS predicate</span>
|
||||
@@ -193,15 +188,20 @@ ROWNUM <= :row_limit</pre>
|
||||
<p class="form-hint mb-0" th:if="${vectorSearch}">선택한 사용자의 직접 역할·그룹 상속 역할에 연결된 permission rule에서 계산됩니다. 기본 whitelist 역할의 ALL은 추가 행 필터 없이 조회를 허용하고, 실제로 TAG·부서 조건을 등록한 역할만 그 조건이 SQL에 들어갑니다. 권한이 없으면 <code>1 = 0</code>입니다.</p>
|
||||
</section>
|
||||
<section class="probe-exchange" data-sql-trace-field="effective_sql">
|
||||
<h3>권한 적용 SQL</h3>
|
||||
<h3>실행 요청 SQL (재현)</h3>
|
||||
<pre th:text="${result.effectiveSql()}">SELECT ... WHERE (...) AND ROWNUM <= ...</pre>
|
||||
</section>
|
||||
</div>
|
||||
<p class="form-hint mt-2 mb-0">
|
||||
위 SQL은 권한 조건을 읽기 쉽게 재현한 표현입니다. 실제 실행 증적은 SQL_ID와 DBMS_XPLAN 영역에서 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
|
||||
이 SQL은 현재 요청의 권한 조건을 읽기 쉽게 재현한 표현입니다. 실제 DB cursor 원문과 적용 predicate는 SQL_ID·DBMS_XPLAN 증적에서 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="alert alert-light mb-0" th:if="${result.successLike() and !result.hasExecutionEvidence() and result.executionEvidenceMessage() != null}">
|
||||
<strong>실제 DB cursor SQL</strong>
|
||||
<span th:text="${result.executionEvidenceMessage()}">최근 SQL_ID를 찾지 못했습니다.</span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-light mb-0" th:if="${result.successLike() and !result.hasSqlTrace()}">
|
||||
이 Handler는 SQL trace 정보를 반환하지 않았고 운영 DB에서도 기본 VPD predicate를 조회하지 못했습니다. <a href="/ords-handlers">조회 연동</a>에서 trace가 포함된 소스를 적용하고, <code>CB_AGENT_DOC_VPD_FILTER</code> 실행 권한을 확인하세요.
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>접근 검증</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>한 사용자의 토큰으로 실제 데이터를 요청해, 설계한 권한이 DB에서 그대로 적용되는지 확인합니다.</p>
|
||||
<p>한 사용자의 토큰으로 실제 데이터를 요청해, 설계한 권한이 DB에서 그대로 적용되는지 확인합니다. 결과의 실행 요청 SQL은 현재 컨텍스트를 결합한 읽기용 표현이며, DB cursor가 기록한 원문 SQL은 <code>V$SQL</code>·<code>DBMS_XPLAN</code> 진단 권한이 있을 때만 함께 표시됩니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small span-2 mb-0">모든 대상에 표준 <code>CB_PERMISSION_SELECT_POLICY</code>와 <code>CB_AGENT_DOC_VPD_FILTER</code>를 적용합니다. 별도 predicate가 필요하면 운영·고급 메뉴의 가이드를 먼저 확인하세요.</p>
|
||||
<p class="text-muted small span-2 mb-0">모든 대상에 표준 <code>CB_PERMISSION_SELECT_POLICY</code>와 <code>CB_AGENT_DOC_VPD_FILTER</code>를 적용합니다. 별도 predicate가 필요하면 관리자 메뉴의 고급 접근 조건 가이드를 먼저 확인하세요.</p>
|
||||
<button class="btn rw-btn-primary" type="submit">선택 스키마에 권한체계 일괄 연결</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
@@ -18,11 +18,18 @@ class GuidedFlowTemplateTest {
|
||||
|
||||
assertThat(html)
|
||||
.contains("th:replace=\"~{fragments/layout :: head('VPD 권한 운영')}\"")
|
||||
.contains("권한 관리")
|
||||
.contains("권한 운영 흐름")
|
||||
.contains("업무 흐름")
|
||||
.contains("dashboard-menu-flow")
|
||||
.contains("도움말: 메뉴와 권한 적용 구조 보기")
|
||||
.contains("CB_APP_USER")
|
||||
.contains("CB_ORDS")
|
||||
.contains("CB_AGENT_CTX")
|
||||
.contains("권한 데이터 모델")
|
||||
.contains("접근 검증")
|
||||
.contains("접근 규칙")
|
||||
.contains("현재 상태")
|
||||
.contains("상세 현황")
|
||||
.contains("등록 대상 상세")
|
||||
.contains("<details class=\"dashboard-details\">")
|
||||
.doesNotContain("journey-card")
|
||||
.doesNotContain("dashboard-workflow")
|
||||
@@ -32,11 +39,13 @@ class GuidedFlowTemplateTest {
|
||||
.contains("권한 관리")
|
||||
.contains("보호·검증")
|
||||
.contains("연동 도구")
|
||||
.contains("운영·고급")
|
||||
.contains("운영")
|
||||
.contains("관리자")
|
||||
.contains("조회 연동")
|
||||
.contains("MCP 서비스")
|
||||
.contains("고급 접근 조건")
|
||||
.contains("시스템 설정")
|
||||
.contains("DB 준비 상태")
|
||||
.contains("data-submenu-trigger")
|
||||
.contains("data-submenu-bar")
|
||||
.contains("data-submenu-panel")
|
||||
@@ -101,7 +110,8 @@ class GuidedFlowTemplateTest {
|
||||
assertThat(result)
|
||||
.contains("적용된 사용자와 권한")
|
||||
.contains("DB 실행 증적")
|
||||
.contains("권한 조건 재현 SQL")
|
||||
.contains("실행 요청 SQL")
|
||||
.contains("실제 DB cursor SQL")
|
||||
.contains("set_vpd_context 사용자 컨텍스트")
|
||||
.contains("CB_AGENT_CTX.USER_ID")
|
||||
.contains("벡터 유사도 검색 기준")
|
||||
@@ -258,8 +268,9 @@ class GuidedFlowTemplateTest {
|
||||
.contains("DB가 기록한 원문 SQL")
|
||||
.contains("DBMS_XPLAN Predicate Information")
|
||||
.contains("SQL_ID")
|
||||
.contains("권한 조건 재현 SQL")
|
||||
.contains("권한 적용 SQL")
|
||||
.contains("실행 요청 SQL")
|
||||
.contains("실제 DB cursor SQL")
|
||||
.contains("실행 요청 SQL (재현)")
|
||||
.contains("VPD가 추가한 WHERE 조건")
|
||||
.contains("ALL: 추가 행 필터 없음")
|
||||
.contains("기본 whitelist 역할");
|
||||
|
||||
Reference in New Issue
Block a user