feat: center DDS demo on sales knowledge authorization

This commit is contained in:
devmrko
2026-06-30 14:21:39 +09:00
parent 188b398dcb
commit 04cca64f10
9 changed files with 270 additions and 55 deletions

View File

@@ -5,15 +5,15 @@
- 기본 포트: `8083` - 기본 포트: `8083`
- 기본 화면: `/` (공통 권한 여정), `/dds` (DDS 직접 조회 검증) - 기본 화면: `/` (공통 권한 여정), `/dds` (DDS 직접 조회 검증)
- 권한 반영: `/dds-provision` (공통 권한을 DDS DATA GRANT로 미리보기·게시) - 권한 반영: `/dds-provision` (공통 권한을 DDS DATA GRANT로 미리보기·게시)
- 기본 보호 객체: `ADMIN.V_DDS_CUSTOMERS_PG`, `ADMIN.V_DDS_CUSTOMERS_MY` - 보호 객체: `ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS` (지식 청크·분류값 검색)
- 지식 검색 보호 객체: `ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS` - 보조 직접 검증 객체: `ADMIN.V_DDS_CUSTOMERS_PG`, `ADMIN.V_DDS_CUSTOMERS_MY`
- DDS 직접 비교 사용자: `dds_demo_my`, `dds_demo_pg`, `dds_demo_both`, `dds_demo_none` - 보조 DDS 비교 사용자: `dds_demo_my`, `dds_demo_pg`, `dds_demo_both`, `dds_demo_none`
- 토큰 경로 기술 사용자: `dds_demo_token` (고객별 DDS 계정이 아님) - 토큰 경로 기술 사용자: `dds_demo_token` (고객별 DDS 계정이 아님)
- 조회 방식: 직접 DDS END USER 비교 경로 + 단일 기술 사용자·Bearer 토큰 경로 - 조회 방식: 직접 DDS END USER 비교 경로 + 단일 기술 사용자·Bearer 토큰 경로
## 두 데모의 관계 ## 두 데모의 관계
두 인스턴스는 프로세스·포트·실행 경계를 분리하지만, 화면에서 설명하는 관리 순서는 같습니다. 두 인스턴스는 프로세스·포트·실행 경계를 분리하지만, 화면에서 설명하는 관리 순서는 같습니다. PG/MySQL 원본 매트릭스는 DDS의 저수준 행/객체 Grant 예제일 뿐이며, 제품형 주 시나리오는 벡터 지식자료입니다.
```text ```text
사용자·그룹·역할 → 데이터 권한 규칙 → DDS Grant 게시 → 보호 연결 → 실제 결과 확인 사용자·그룹·역할 → 데이터 권한 규칙 → DDS Grant 게시 → 보호 연결 → 실제 결과 확인
@@ -58,13 +58,16 @@ ORDS Handler가 Bearer 값을 `cb_dds_hr` 같은 문자열로 바꾸는 것만
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/31_dds_standalone_demo_setup.sql sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/31_dds_standalone_demo_setup.sql
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/32_dds_vector_tag_setup.sql sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/32_dds_vector_tag_setup.sql
bash scripts/setup-dds-token-data-grant.sh bash scripts/setup-dds-token-data-grant.sh
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/36_dds_sales_knowledge_scenario.sql
``` ```
`32_dds_vector_tag_setup.sql`은 공통 청크·태그 저장소를 읽는 DDS 전용 VIEW를 만들고, 동일한 `CB_PERMISSION`/`CB_PERMISSION_RULE`에 역할별 TAG 규칙을 등록합니다. `SPRING_BOOT`, `ORDS`, `ORACLE_VPD`, `MCP`처럼 한 역할에 여러 TAG가 있으면 `/dds-provision`이 OR 조건으로 합쳐 DDS DATA GRANT를 다시 만듭니다. 벡터 검색에 필요한 `EMBEDDING`은 DDS 엔진이 거리 계산에 사용하므로 Grant에서 제외하지 않고, 검색 응답에서는 애플리케이션이 반환하지 않습니다. 그 외 민감 컬럼은 애플리케이션 권한의 원문 허용 목록에 없으면 `ALL COLUMNS EXCEPT`로 제외됩니다. 검색 화면에서는 같은 문서를 청킹·임베딩한 뒤 선택한 END USER로 직접 조회하므로, DDS Grant가 없는 주체는 보호 VIEW 자체를 볼 수 없습니다. `32_dds_vector_tag_setup.sql`은 공통 청크·태그 저장소를 읽는 DDS 전용 VIEW를 만들고, 동일한 `CB_PERMISSION`/`CB_PERMISSION_RULE`에 역할별 TAG 규칙을 등록합니다. `SPRING_BOOT`, `ORDS`, `ORACLE_VPD`, `MCP`처럼 한 역할에 여러 TAG가 있으면 `/dds-provision`이 OR 조건으로 합쳐 DDS DATA GRANT를 다시 만듭니다. 벡터 검색에 필요한 `EMBEDDING`은 DDS 엔진이 거리 계산에 사용하므로 Grant에서 제외하지 않고, 검색 응답에서는 애플리케이션이 반환하지 않습니다. 그 외 민감 컬럼은 애플리케이션 권한의 원문 허용 목록에 없으면 `ALL COLUMNS EXCEPT`로 제외됩니다. 검색 화면에서는 같은 문서를 청킹·임베딩한 뒤 선택한 END USER로 직접 조회하므로, DDS Grant가 없는 주체는 보호 VIEW 자체를 볼 수 없습니다.
`34_dds_token_data_grant_common_auth.sql``dds_demo_token` 하나와 `cb_dds_token_role` 하나를 만들고, `ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS`에 객체별 Data Grant를 연결합니다. Data Grant의 predicate 함수는 직접 역할과 활성 그룹 역할을 합친 뒤 `CB_PERMISSION`·`CB_PERMISSION_RULE`의 TAG 허용/거부 규칙을 요청 시 평가합니다. 토큰은 Data Grant 문법의 인자가 아니라 `CB_AGENT_CTX`를 초기화하는 입력입니다. `34_dds_token_data_grant_common_auth.sql``dds_demo_token` 하나와 `cb_dds_token_role` 하나를 만들고, `ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS`에 객체별 Data Grant를 연결합니다. Data Grant의 predicate 함수는 직접 역할과 활성 그룹 역할을 합친 뒤 `CB_PERMISSION`·`CB_PERMISSION_RULE`의 TAG 허용/거부 규칙을 요청 시 평가합니다. 토큰은 Data Grant 문법의 인자가 아니라 `CB_AGENT_CTX`를 초기화하는 입력입니다.
토큰 경로를 제거할 때는 사용 중인 애플리케이션이 없는지 확인한 뒤 `sql/adb/35_dds_token_data_grant_cleanup.sql`을 별도로 승인해 실행합니다. `36_dds_sales_knowledge_scenario.sql``agent_sales``SALES_KNOWLEDGE_ROLE`을 만들고 `TECH_TAG=SALES`인 세일즈 청크(28004)를 추가합니다. 짧은 데모 토큰 `dds_sales_demo_token`을 사용하면 결과는 28004만 남아야 합니다. 운영에서는 이 토큰 대신 Bearer 관리 화면에서 발급한 임시 토큰을 사용합니다.
토큰 경로를 제거할 때는 사용 중인 애플리케이션이 없는지 확인한 뒤 `sql/adb/37_dds_sales_knowledge_scenario_cleanup.sql``sql/adb/35_dds_token_data_grant_cleanup.sql`을 별도로 승인해 실행합니다.
그 다음 DDS 인스턴스를 실행합니다. 그 다음 DDS 인스턴스를 실행합니다.

View File

@@ -37,10 +37,10 @@
</section> </section>
<section class="content-band"> <section class="content-band">
<div class="section-heading"><div><h2>현재 DDS 검증 대상</h2><p class="section-subtitle">고객 데이터 원본과 벡터 지식자료를 각각 DDS 전용 객체로 확인합니다.</p></div></div> <div class="section-heading"><div><h2>현재 DDS 검증 대상</h2><p class="section-subtitle">주 시나리오는 업무 분류값이 붙은 벡터 지식자료 검색입니다. 원본 테이블은 보조 기술 검증으로만 제공합니다.</p></div></div>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>대상</th><th>보호 방식</th><th>검증</th></tr></thead><tbody> <div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>대상</th><th>보호 방식</th><th>검증</th></tr></thead><tbody>
<tr><td><code>ADMIN.V_DDS_CUSTOMERS_PG/MY</code></td><td>DATA ROLE/DATA GRANT</td><td><a href="/dds">원본별 직접 조회</a></td></tr> <tr><td><code th:text="${vectorObject}">ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS</code></td><td>객체별 분류값 DATA GRANT + 토큰 Context + VECTOR_DISTANCE</td><td><a href="/vector-knowledge">지식 검색</a></td></tr>
<tr><td><code th:text="${vectorObject}">ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS</code></td><td>객체별 TAG DATA GRANT + 토큰 Context + VECTOR_DISTANCE</td><td><a href="/vector-knowledge">지식 검색</a></td></tr> <tr><td>PG/MY 원본 예제</td><td>보조 END USER 직접 Grant</td><td><a href="/dds">저수준 검증 열기</a></td></tr>
</tbody></table></div> </tbody></table></div>
</section> </section>
</main> </main>

View File

@@ -7,12 +7,12 @@
<main class="container py-4"> <main class="container py-4">
<header class="hero"> <header class="hero">
<span class="eyebrow">DEEP DATA SECURITY · INDEPENDENT DEMO</span> <span class="eyebrow">DEEP DATA SECURITY · INDEPENDENT DEMO</span>
<h1>같은 접근 결과를 다른 보안 모델로 확인합니다.</h1> <h1>업무 사용자의 지식 접근 범위를 DDS로 확인합니다.</h1>
<p class="lead">이 화면은 VPD 데모와 별도로 실행되는 DDS 전용 데모입니다. 선택한 END USER로 직접 접속해 Oracle의 DATA ROLE과 DATA GRANT가 적용된 결과를 보여줍니다.</p> <p class="lead">이 화면은 VPD 데모와 별도로 실행되는 DDS 보조 검증 화면입니다. 실제 시나리오는 세일즈 사용자의 토큰이 `TECH_TAG=SALES`인 지식 청크만 검색하도록 제한하는 것입니다.</p>
<details class="explanation"> <details class="explanation">
<summary>이 화면의 보안 경계</summary> <summary>이 화면의 보안 경계</summary>
<p>이 인스턴스는 VPD 권한 테이블을 읽어 조건을 계산하지 않습니다. <code>END USER → DATA ROLE → DATA GRANT → 보호 VIEW</code>가 전부 Oracle DDS에 선언되어 있고, 조회 결과는 그 선언의 적용 결과입니다.</p> <p>문서를 청킹·임베딩할 때 각 청크의 <code>TECH_TAG</code>에 업무 분류값을 저장합니다. 권한 관리에서는 역할에 허용할 값을 등록하고, DDS Data Grant가 보호 VIEW의 해당 컬럼 값을 predicate로 평가합니다.</p>
<p class="mb-0">두 데모는 같은 접근 매트릭스를 비교하지만 실행 모델은 다릅니다. VPD는 공통 계정·Bearer·동적 predicate를 사용하고, DDS는 직접 END USER와 선언형 DATA GRANT를 사용합니다. Bearer 키를 ORDS Handler에서 사용자 이름으로 바꾸는 것만으로는 DDS 보안 컨텍스트가 만들어지지 않습니다.</p> <p class="mb-0">예를 들어 세일즈 사용자의 토큰이 업무 사용자 <code>agent_sales</code>로 매핑되고 역할에 <code>ALLOW TAG SALES</code>가 있으면, 검색 결과에는 <code>TECH_TAG</code>에 SALES가 포함된 청크만 남습니다.</p>
</details> </details>
</header> </header>
@@ -21,15 +21,15 @@
<section class="flow-grid" aria-label="DDS 접근 흐름"> <section class="flow-grid" aria-label="DDS 접근 흐름">
<article class="flow-card"> <article class="flow-card">
<span class="flow-number">1</span> <span class="flow-number">1</span>
<div><span class="eyebrow">IDENTITY</span><h2>END USER</h2><p>실제 DDS 보안 사용자로 DB에 접속합니다.</p></div> <div><span class="eyebrow">IDENTITY</span><h2>업무 사용자 토큰</h2><p>Bearer 토큰을 세일즈 업무 사용자로 매핑합니다.</p></div>
</article> </article>
<article class="flow-card"> <article class="flow-card">
<span class="flow-number">2</span> <span class="flow-number">2</span>
<div><span class="eyebrow">POLICY</span><h2>DATA ROLE / GRANT</h2><p>원본별 허용 범위가 선언형으로 연결니다.</p></div> <div><span class="eyebrow">POLICY</span><h2>객체별 DATA GRANT</h2><p>보호 VIEW의 컬럼 값과 공통 권한 규칙을 연결니다.</p></div>
</article> </article>
<article class="flow-card"> <article class="flow-card">
<span class="flow-number">3</span> <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> <div><span class="flow-number result-number">3</span><span class="eyebrow">EVIDENCE</span><h2>허용된 지식</h2><p>SALES 값이 붙은 청크만 벡터 검색 결과로 반환됩니다.</p></div>
</article> </article>
</section> </section>
@@ -38,7 +38,7 @@
<div> <div>
<span class="eyebrow">SAME MANAGEMENT FLOW</span> <span class="eyebrow">SAME MANAGEMENT FLOW</span>
<h2>VPD 데모와 같은 권한 관리 흐름</h2> <h2>VPD 데모와 같은 권한 관리 흐름</h2>
<p>사용자·그룹·역할·데이터 권한은 같은 순서로 설계하고, 보호 적용 단계만 DDS 선언형 객체로 연결합니다.</p> <p>사용자·그룹·역할·데이터 권한은 같은 순서로 설계하고, 문서 청크의 분류값과 보호 VIEW를 DDS Data Grant로 연결합니다.</p>
</div> </div>
</div> </div>
<div class="matrix-grid"> <div class="matrix-grid">
@@ -50,15 +50,17 @@
</section> </section>
<div class="alert alert-warning" th:if="${configuredUserCount == 0}"> <div class="alert alert-warning" th:if="${configuredUserCount == 0}">
DDS 사용자 비밀번호가 설정되지 않았습니다. VM에서는 <code>DDSUSER_*_PASSWORD</code> 또는 <code>DDS_BACKOFFICE_*_PASSWORD</code> 환경 변수를 설정해야 조회할 수 있습니다. DDS 직접 비교 사용자 비밀번호가 설정되지 않았습니다. 주 시나리오인 토큰 검색은 <code>/vector-knowledge</code>에서 기술 사용자 설정을 별도로 확인합니다.
</div> </div>
<details class="explanation mb-4">
<summary>저수준 PG/MY DDS 예제 열기</summary>
<section class="panel"> <section class="panel">
<div class="panel-heading"> <div class="panel-heading">
<div> <div>
<span class="eyebrow">1 · SECURITY SUBJECT</span> <span class="eyebrow">1 · SECURITY SUBJECT</span>
<h2>조회 주체와 데이터 원본 선택</h2> <h2>저수준 DDS 직접 검증</h2>
<p>선택한 DDS END USER의 권한으로 직접 연결합니다. 비밀번호는 화면에 표시하거나 저장하지 않습니다.</p> <p>PG/MY는 제품 시나리오가 아니라 DDS의 객체별 직접 Grant 동작을 확인하기 위한 보조 예제입니다.</p>
</div> </div>
</div> </div>
<form method="post" action="/dds/query" class="query-grid"> <form method="post" action="/dds/query" class="query-grid">
@@ -96,8 +98,8 @@
<div class="panel-heading"> <div class="panel-heading">
<div> <div>
<span class="eyebrow">2 · EXPECTED MATRIX</span> <span class="eyebrow">2 · EXPECTED MATRIX</span>
<h2>원본 권한 매트릭스</h2> <h2>보조 원본 권한 매트릭스</h2>
<p>VPD 데모와 같은 결과를 별도 데이터·권한 객체에서 DDS 선언형 모델로 비교합니다.</p> <p>아래 PG/MY 매트릭스는 지식 검색 시나리오와 분리된 DDS 저수준 테스트 데이터입니다.</p>
</div> </div>
</div> </div>
<div class="matrix-grid"> <div class="matrix-grid">
@@ -107,6 +109,7 @@
<div class="matrix-card muted"><strong>dds_demo_none</strong><span>접속만 허용</span><small>데이터 권한 없음 · default deny</small></div> <div class="matrix-card muted"><strong>dds_demo_none</strong><span>접속만 허용</span><small>데이터 권한 없음 · default deny</small></div>
</div> </div>
</section> </section>
</details>
<section class="panel" th:if="${queryResult != null}"> <section class="panel" th:if="${queryResult != null}">
<div class="panel-heading result-heading"> <div class="panel-heading result-heading">

View File

@@ -15,7 +15,7 @@
</div> </div>
<div class="table-responsive mt-3" th:if="${searchResult.success() and searchResult.hasRows()}"> <div class="table-responsive mt-3" th:if="${searchResult.success() and searchResult.hasRows()}">
<table class="table table-sm align-middle"> <table class="table table-sm align-middle">
<thead><tr><th>검색 단위</th><th>자료 ID</th><th>제목</th><th>기술 태그</th><th>관련도</th><th>본문</th></tr></thead> <thead><tr><th>검색 단위</th><th>자료 ID</th><th>제목</th><th>업무 분류값</th><th>관련도</th><th>본문</th></tr></thead>
<tbody> <tbody>
<tr th:each="row : ${searchResult.rows()}"> <tr th:each="row : ${searchResult.rows()}">
<td th:text="${row['CHUNK_ID']}">28001</td> <td th:text="${row['CHUNK_ID']}">28001</td>

View File

@@ -10,7 +10,7 @@
<p class="context-summary">문서 → 청크·임베딩 → 기술 태그 → 권한 규칙 → DDS DATA GRANT → 검색 결과</p> <p class="context-summary">문서 → 청크·임베딩 → 기술 태그 → 권한 규칙 → DDS DATA GRANT → 검색 결과</p>
<details class="explanation-details"> <details class="explanation-details">
<summary>이 화면의 큰 흐름 보기</summary> <summary>이 화면의 큰 흐름 보기</summary>
<p>문서는 검색 가능한 청크로 나뉘고 각 청크에 기술 태그가 붙습니다. 권한 관리에서 역할별로 허용할 태그를 등록하면 DDS의 DATA GRANT가 해당 태그가 붙은 청크만 검색 대상으로 남깁니다.</p> <p>문서는 검색 가능한 청크로 나뉘고 각 청크에 업무 접근 분류값(<code>TECH_TAG</code>)이 붙습니다. 권한 관리에서 역할별로 허용할 값을 등록하면 DDS의 DATA GRANT가 해당 값이 붙은 청크만 검색 대상으로 남깁니다.</p>
<p class="mb-0">VPD 화면과 같은 사용자·그룹·역할·권한 규칙을 사용합니다. 아래에는 DDS END USER를 직접 선택하는 비교 경로와, 하나의 DDS 기술 사용자에 Bearer 토큰을 전달해 공통 권한을 평가하는 제품형 경로를 함께 둡니다.</p> <p class="mb-0">VPD 화면과 같은 사용자·그룹·역할·권한 규칙을 사용합니다. 아래에는 DDS END USER를 직접 선택하는 비교 경로와, 하나의 DDS 기술 사용자에 Bearer 토큰을 전달해 공통 권한을 평가하는 제품형 경로를 함께 둡니다.</p>
</details> </details>
</div> </div>
@@ -45,9 +45,9 @@
<label>문서 ID<input class="form-control" name="documentId" placeholder="knowledge-security-001" required></label> <label>문서 ID<input class="form-control" name="documentId" placeholder="knowledge-security-001" required></label>
<label>문서 제목<input class="form-control" name="title" placeholder="DDS 기술 태그 권한 설계" required></label> <label>문서 제목<input class="form-control" name="title" placeholder="DDS 기술 태그 권한 설계" required></label>
<label>원문 주소 (선택)<input class="form-control" name="sourceUri" placeholder="kb://security/dds-vector"></label> <label>원문 주소 (선택)<input class="form-control" name="sourceUri" placeholder="kb://security/dds-vector"></label>
<label>기술 태그 (쉼표 또는 공백) <label>지식 접근 분류값 (쉼표 또는 공백)
<input class="form-control" name="techTags" placeholder="SPRING_BOOT ORACLE_DDS" required> <input class="form-control" name="techTags" placeholder="SALES INTERNAL" required>
<span class="form-hint">태그는 대문자로 정규화됩니다. 권한 규칙의 TAG 값과 일치해야 합니다.</span> <span class="form-hint">예: SALES. 값은 대문자로 정규화되고 권한 규칙의 TAG 값과 일치해야 합니다.</span>
</label> </label>
<label>검색 단위 길이<input class="form-control" name="chunkSize" type="number" min="80" max="2000" value="600"></label> <label>검색 단위 길이<input class="form-control" name="chunkSize" type="number" min="80" max="2000" value="600"></label>
<label>임베딩 방식 <label>임베딩 방식
@@ -80,8 +80,8 @@
</div> </div>
</div> </div>
<div class="macro-micro-grid"> <div class="macro-micro-grid">
<div><h3>관리 관점</h3><p>역할에 <code>ALLOW TAG</code> 여러 개 등록하면 태그 중 하나라도 맞는 청크 허용합니다. <code>DENY TAG</code>는 허용 후보에서 제외합니다.</p></div> <div><h3>관리 관점</h3><p>예를 들어 세일즈 역할에 <code>ALLOW TAG SALES</code>를 등록하면 SALES 값이 붙은 청크 허용합니다. <code>DENY TAG</code>는 허용 후보에서 제외합니다.</p></div>
<div><h3>DDS 실행 관점</h3><p><code th:text="${ddsVectorObject}">ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS</code>에 연결된 DATA GRANT가 END USER의 DATA ROLE을 통해 이 규칙을 집행합니다.</p></div> <div><h3>DDS 실행 관점</h3><p><code th:text="${ddsVectorObject}">ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS</code>의 객체별 DATA GRANT predicate가 토큰으로 식별된 업무 사용자와 청크의 <code>TECH_TAG</code> 값을 비교해 이 규칙을 집행합니다.</p></div>
</div> </div>
<details class="explanation-details mt-3"> <details class="explanation-details mt-3">
<summary>VPD와 DDS의 차이 보기</summary> <summary>VPD와 DDS의 차이 보기</summary>
@@ -106,7 +106,7 @@
<li><code>CB_DDS_VECTOR_SEARCH_DOCUMENTS</code>의 객체별 DATA GRANT predicate가 <code>CB_APP_USER</code>, 역할·그룹, <code>CB_PERMISSION</code>, <code>CB_PERMISSION_RULE</code>을 조회합니다.</li> <li><code>CB_DDS_VECTOR_SEARCH_DOCUMENTS</code>의 객체별 DATA GRANT predicate가 <code>CB_APP_USER</code>, 역할·그룹, <code>CB_PERMISSION</code>, <code>CB_PERMISSION_RULE</code>을 조회합니다.</li>
<li>ALLOW TAG는 OR로 합치고 DENY TAG는 최종 결과에서 제외합니다.</li> <li>ALLOW TAG는 OR로 합치고 DENY TAG는 최종 결과에서 제외합니다.</li>
</ol> </ol>
<p class="mb-0">토큰은 DATA GRANT 문법의 바인드 파라미터가 아닙니다. 토큰이 만든 신뢰된 Context를 Data Grant predicate가 참조하는 구조입니다. 토큰은 저장하거나 화면에 재표시하지 않습니다.</p> <p class="mb-0">세일즈 예제에서는 <code>agent_sales</code> 역할이 <code>ALLOW TAG SALES</code>를 가지고, 청크의 <code>TECH_TAG</code>에 SALES가 포함된 지식만 남습니다. 토큰은 DATA GRANT 문법의 바인드 파라미터가 아니라 Context를 만드는 입력이며 저장하지 않습니다.</p>
</details> </details>
<form hx-post="/vector-knowledge/token-search" hx-target="#vector-token-search-result" hx-swap="innerHTML" class="form-grid mt-3"> <form hx-post="/vector-knowledge/token-search" hx-target="#vector-token-search-result" hx-swap="innerHTML" class="form-grid mt-3">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
@@ -122,7 +122,7 @@
</select> </select>
</label> </label>
<label class="span-2">검색 질문 <label class="span-2">검색 질문
<textarea class="form-control" name="query" rows="3" placeholder="예: ORACLE_VPD 태그가 있는 지식자료의 접근 조건" required></textarea> <textarea class="form-control" name="query" rows="3" placeholder="예: 세일즈 파이프라인 후속 조치 기준" required></textarea>
</label> </label>
<button class="btn rw-btn-primary" type="submit">토큰 권한으로 검색</button> <button class="btn rw-btn-primary" type="submit">토큰 권한으로 검색</button>
</form> </form>

View File

@@ -1,13 +1,13 @@
# 설계서: 기술 태그 기반 벡터 지식자료 검색과 VPD 연결 # 설계서: 기술 태그 기반 벡터 지식자료 검색과 VPD 연결
> **상태**: 제품형 운영 흐름 구현 · DDS 병행 검증 완료 · 객체별 Data Grant predicate 적용 · 운영 확장 항목 별도 > **상태**: 제품형 운영 흐름 구현 · DDS 병행 검증 완료 · 객체별 Data Grant predicate 적용 · 운영 확장 항목 별도
> **추적성**: Redmine #565, #566 · 구현 커밋 `a5e70bc` · 기준 구현: `28_agent_ords_vector_tag_vpd_setup.sql`, `29_agent_ords_vector_search_ords.sql`, `32_dds_vector_tag_setup.sql`, `34_dds_token_data_grant_common_auth.sql` > **추적성**: Redmine #565, #566 · 구현 커밋 `a5e70bc` · 기준 구현: `28_agent_ords_vector_tag_vpd_setup.sql`, `29_agent_ords_vector_search_ords.sql`, `32_dds_vector_tag_setup.sql`, `34_dds_token_data_grant_common_auth.sql`, `36_dds_sales_knowledge_scenario.sql`
## 1. 한 문장으로 이해하기 ## 1. 한 문장으로 이해하기
문서를 잘게 나누고(청킹) 숫자 벡터로 바꾼 뒤(벡터화) 각 조각에 `SPRING_BOOT`, `ORACLE_VPD` 같은 기술 태그를 붙인다. 사용자의 권한에는 “이 태그를 볼 수 있음”을 등록하고, ORDS 검색은 권한에 맞는 조각만 검색 결과에 남긴다. 문서를 잘게 나누고(청킹) 숫자 벡터로 바꾼 뒤(벡터화) 각 조각에 `SALES`, `HR`, `FINANCE` 같은 업무 접근 분류값을 붙인다. 사용자의 권한에는 “이 값을 볼 수 있음”을 등록하고, 검색은 권한에 맞는 조각만 검색 결과에 남긴다.
이 기능에서 태그는 문서의 분류표이고, VPD는 그 분류표를 이용해 DB가 실제로 보여 줄 행을 제한하는 장치다. 화면에서 검색 조건을 넣는 것만으로는 우회할 수 없도록 DB 안에서 마지막 필터를 적용한다. 이 기능에서 `TECH_TAG`는 문서 청크에 저장된 업무 분류 컬럼 값이고, VPD/DDS는 그 값을 이용해 DB가 실제로 보여 줄 행을 제한하는 장치다. 화면에서 검색 조건을 넣는 것만으로는 우회할 수 없도록 DB 안에서 마지막 필터를 적용한다.
## 2. 큰 흐름 (일반 사용자가 보는 순서) ## 2. 큰 흐름 (일반 사용자가 보는 순서)
@@ -51,11 +51,11 @@
- 같은 `ALLOW` 권한에 태그를 여러 개 넣으면 OR이다. - 같은 `ALLOW` 권한에 태그를 여러 개 넣으면 OR이다.
```text ```text
ALLOW TAG SPRING_BOOT ALLOW TAG SALES
ALLOW TAG ORACLE_VPD ALLOW TAG INTERNAL
결과: TECH_TAG 목록에 SPRING_BOOT가 포함됨 결과: TECH_TAG 목록에 SALES가 포함됨
OR TECH_TAG 목록에 ORACLE_VPD가 포함됨 OR TECH_TAG 목록에 INTERNAL이 포함됨
``` ```
- `DENY` 태그도 내부적으로 OR로 묶은 뒤 허용 결과에서 뺀다. - `DENY` 태그도 내부적으로 OR로 묶은 뒤 허용 결과에서 뺀다.
@@ -74,21 +74,21 @@ AND NOT (거부 태그 C OR 거부 태그 D)
| 청크 | 태그 | | 청크 | 태그 |
|---|---| |---|---|
| Spring Boot VPD 시작하기 | `SPRING_BOOT`, `ORDS` | | 세일즈 파이프라인 운영 가이드 | `SALES`, `INTERNAL` |
| Oracle VPD 정책 연결 | `ORACLE_VPD`, `ORDS` | | 인사 채용 운영 기준 | `HR`, `INTERNAL` |
| MCP 도구 권한 설계 | `MCP` | | 재무 마감 체크리스트 | `FINANCE`, `INTERNAL` |
역할을 다음처럼 만든다. 역할을 다음처럼 만든다.
1. `KNOWLEDGE_BACKEND`: `ALLOW TAG SPRING_BOOT`, `ALLOW TAG ORACLE_VPD` 1. `SALES_KNOWLEDGE_ROLE`: `ALLOW TAG SALES`
2. `KNOWLEDGE_MCP`: `ALLOW TAG MCP` 2. `HR_KNOWLEDGE_ROLE`: `ALLOW TAG HR`
3. `KNOWLEDGE_NO_ORDS`: `ALLOW TAG SPRING_BOOT`, `ALLOW TAG ORACLE_VPD`, `DENY TAG ORDS` 3. `KNOWLEDGE_INTERNAL`: `ALLOW TAG INTERNAL`, 필요 시 `DENY TAG FINANCE`
그러면: 그러면:
- `KNOWLEDGE_BACKEND` 사용자는 첫 번째와 두 번째 청크 본다. - `agent_sales` 사용자는 `SALES` 값이 붙은 세일즈 청크 본다.
- `KNOWLEDGE_MCP` 사용자는 세 번째 청크만 본다. - `agent_hr` 사용자는 `HR` 값이 붙은 청크만 본다.
- `KNOWLEDGE_NO_ORDS` 사용자는 `ORDS` 태그가 붙은 첫 번째·두 번째 청크가 거부되어 결과가 없다. - `KNOWLEDGE_INTERNAL` 사용자는 `INTERNAL` 값이 붙은 청크를 보되, `FINANCE` 거부 규칙이 있으면 재무 청크는 제외된다.
- 태그 권한이 전혀 없는 사용자는 결과가 없다. - 태그 권한이 전혀 없는 사용자는 결과가 없다.
## 5. ORDS API 계약 ## 5. ORDS API 계약
@@ -114,10 +114,10 @@ Content-Type: application/json
"chunk_id": 28001, "chunk_id": 28001,
"document_id": "knowledge-001", "document_id": "knowledge-001",
"chunk_no": 1, "chunk_no": 1,
"title": "Spring Boot VPD 시작하기", "title": "세일즈 파이프라인 운영 가이드",
"chunk_text": "...", "chunk_text": "...",
"source_uri": "kb://security/spring-boot-vpd", "source_uri": "kb://business/sales/pipeline",
"tech_tag": "ORDS,SPRING_BOOT", "tech_tag": "INTERNAL,SALES",
"score": 0.0123 "score": 0.0123
} }
] ]
@@ -137,6 +137,8 @@ Content-Type: application/json
5. `29_agent_ords_vector_search_ords.sql``cb-agent-vector/search` ORDS 모듈/핸들러 (CB_ORDS로 실행) 5. `29_agent_ords_vector_search_ords.sql``cb-agent-vector/search` ORDS 모듈/핸들러 (CB_ORDS로 실행)
6. backoffice `/permissions`에서 역할별 `특정 기술 태그` 규칙을 저장 6. backoffice `/permissions`에서 역할별 `특정 기술 태그` 규칙을 저장
7. `30_agent_ords_vector_tag_vpd_test.sql` — OR와 DENY 우선순위 확인 7. `30_agent_ords_vector_tag_vpd_test.sql` — OR와 DENY 우선순위 확인
8. DDS 기본 경로: `34_dds_token_data_grant_common_auth.sql` — 단일 기술 사용자·객체별 Data Grant predicate
9. 세일즈 시나리오: `36_dds_sales_knowledge_scenario.sql``agent_sales` + `TECH_TAG=SALES` + 단기 데모 토큰
backoffice의 `/objects` 화면은 이 객체에 일반 Handler를 생성하지 않도록 막는다. 일반 Handler를 사용하면 벡터 컬럼을 그대로 반환할 수 있기 때문에, 임베딩을 응답에서 제외하고 벡터 거리 검색을 하는 전용 Handler만 허용한다. backoffice의 `/objects` 화면은 이 객체에 일반 Handler를 생성하지 않도록 막는다. 일반 Handler를 사용하면 벡터 컬럼을 그대로 반환할 수 있기 때문에, 임베딩을 응답에서 제외하고 벡터 거리 검색을 하는 전용 Handler만 허용한다.
@@ -160,11 +162,11 @@ backoffice의 `/objects` 화면은 이 객체에 일반 Handler를 생성하지
## 7. 운영 가이드라인 ## 7. 운영 가이드라인
- 태그는 자유 문장보다 대문자·언더스코어 형태의 안정적인 ID로 관리한다. 예: `SPRING_BOOT`, `ORACLE_VPD`, `INTERNAL_ONLY`. - 값은 자유 문장보다 대문자·언더스코어 형태의 안정적인 ID로 관리한다. 예: `SALES`, `HR`, `FINANCE`, `INTERNAL`.
- 현재 예시의 태그 ID 허용 문자는 영문·숫자·`_`·`-`다. 이 범위를 벗어난 값은 UI와 VPD 함수에서 모두 무시해 권한이 넓어지지 않게 한다. - 현재 예시의 태그 ID 허용 문자는 영문·숫자·`_`·`-`다. 이 범위를 벗어난 값은 UI와 VPD 함수에서 모두 무시해 권한이 넓어지지 않게 한다.
- 동의어를 무분별하게 만들지 말고 태그 사전을 먼저 정한다. `SPRING`, `SPRINGBOOT`, `SPRING_BOOT`을 모두 별개로 만들면 권한 누락이 생긴다. - 동의어를 무분별하게 만들지 말고 분류값 사전을 먼저 정한다. `SALES`, `SALES_TEAM`, `SELLING`을 모두 별개로 만들면 권한 누락이 생긴다.
- 문서 청크에 최소 한 개의 기술 태그를 부여한다. 태그가 없는 청크는 이 뷰에 노출되지 않는다. - 문서 청크에 최소 한 개의 업무 분류값을 부여한다. 값이 없는 청크는 이 뷰에 노출되지 않는다.
- 태그를 수정하면 권한 결과가 즉시 바뀐다. 임베딩을 다시 만들 필요가 없는 태그 변경과, 내용이 바뀌어 재임베딩해야 하는 변경을 구분한다. - 분류값을 수정하면 권한 결과가 즉시 바뀐다. 임베딩을 다시 만들 필요가 없는 변경과, 내용이 바뀌어 재임베딩해야 하는 변경을 구분한다.
- `TECH_TAG`는 행 접근용이고, 컬럼 민감도/마스킹은 별도 정책이다. “컬럼을 PUBLIC으로 표시했다”는 사실이 행 조회 권한을 부여하지 않는다. - `TECH_TAG`는 행 접근용이고, 컬럼 민감도/마스킹은 별도 정책이다. “컬럼을 PUBLIC으로 표시했다”는 사실이 행 조회 권한을 부여하지 않는다.
- 벡터 차원은 ingestion 모델과 테이블 데이터가 일치해야 한다. 차원이 다르면 ORDS가 4xx/5xx 오류를 반환하므로 모델 버전을 함께 기록한다. - 벡터 차원은 ingestion 모델과 테이블 데이터가 일치해야 한다. 차원이 다르면 ORDS가 4xx/5xx 오류를 반환하므로 모델 버전을 함께 기록한다.
@@ -203,6 +205,7 @@ Bearer token
| `agent_hr` | `28001`, `28002` (2건) | | `agent_hr` | `28001`, `28002` (2건) |
| `agent_fin_self` | `28002` (1건) | | `agent_fin_self` | `28002` (1건) |
| `agent_all` | `28001`, `28002`, `28003` (3건) | | `agent_all` | `28001`, `28002`, `28003` (3건) |
| `agent_sales` | `28004` (1건, `TECH_TAG=SALES`) |
이 방식의 의미는 “DDS가 토큰 문자열을 Data Grant 문법의 인자로 받는다”가 아니다. `DATA GRANT``ON` 대상 뷰와 저장된 SQL predicate를 선언하고, 토큰 함수가 그 predicate가 참조할 Context를 만든다. 따라서 현재 제품형 기본 시나리오는 **단일 DDS 기술 사용자 + 객체별 DATA GRANT predicate + 공통 업무 권한**이다. 여러 보호 뷰/테이블에는 각각 Data Grant를 만들고, 각 predicate에서 논리 권한 대상과 행 규칙을 매핑한다. 드라이버가 SQL 실행 전에 `EndUserSecurityContext`를 전달하는 순수 DDS Context 방식은 별도 확장 경계이며, 그 경우에는 `ORA_END_USER_CONTEXT`를 참조하는 일반 `DATA GRANT`를 설계한다. 이 방식의 의미는 “DDS가 토큰 문자열을 Data Grant 문법의 인자로 받는다”가 아니다. `DATA GRANT``ON` 대상 뷰와 저장된 SQL predicate를 선언하고, 토큰 함수가 그 predicate가 참조할 Context를 만든다. 따라서 현재 제품형 기본 시나리오는 **단일 DDS 기술 사용자 + 객체별 DATA GRANT predicate + 공통 업무 권한**이다. 여러 보호 뷰/테이블에는 각각 Data Grant를 만들고, 각 predicate에서 논리 권한 대상과 행 규칙을 매핑한다. 드라이버가 SQL 실행 전에 `EndUserSecurityContext`를 전달하는 순수 DDS Context 방식은 별도 확장 경계이며, 그 경우에는 `ORA_END_USER_CONTEXT`를 참조하는 일반 `DATA GRANT`를 설계한다.

View File

@@ -2,12 +2,26 @@
> **상태**: VPD·DDS 병행 검증 완료, 토큰 기반 객체별 Data Grant 적용 완료 > **상태**: VPD·DDS 병행 검증 완료, 토큰 기반 객체별 Data Grant 적용 완료
> **추적성**: Redmine #565, #566 · DDS 구현 커밋 `1e48864`, `a5e70bc` > **추적성**: Redmine #565, #566 · DDS 구현 커밋 `1e48864`, `a5e70bc`
> **기준 구현**: `sql/adb/32_dds_vector_tag_setup.sql`, `sql/adb/34_dds_token_data_grant_common_auth.sql`, `dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/service/DdsGrantPublisher.java`, `dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/service/DdsVectorKnowledgeService.java` > **기준 구현**: `sql/adb/32_dds_vector_tag_setup.sql`, `sql/adb/34_dds_token_data_grant_common_auth.sql`, `sql/adb/36_dds_sales_knowledge_scenario.sql`, `dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/service/DdsGrantPublisher.java`, `dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/service/DdsVectorKnowledgeService.java`
## 목적 ## 목적
VPD와 Oracle Deep Data Security(DDS)를 별도 인스턴스로 운영하면서도, 고객의 업무 권한을 정의하는 공통 테이블을 기준으로 같은 검색 시나리오를 비교한다. 고객마다 DDS 계정을 만드는 것이 목표가 아니다. 현재 기본 데모는 하나의 DDS 기술 사용자 세션에서 Bearer 토큰으로 Context를 만들고, 객체별 Data Grant predicate가 공통 권한 테이블을 읽어 결과를 제한한다. VPD와 Oracle Deep Data Security(DDS)를 별도 인스턴스로 운영하면서도, 고객의 업무 권한을 정의하는 공통 테이블을 기준으로 같은 검색 시나리오를 비교한다. 고객마다 DDS 계정을 만드는 것이 목표가 아니다. 현재 기본 데모는 하나의 DDS 기술 사용자 세션에서 Bearer 토큰으로 Context를 만들고, 객체별 Data Grant predicate가 공통 권한 테이블을 읽어 결과를 제한한다.
## 주 시나리오: 세일즈 사용자의 지식 검색
PG/MySQL 원본 매트릭스는 DDS의 객체·행 Grant 동작을 설명하기 위한 보조 예제다. 제품 흐름의 주인공은 지식 검색이다.
```text
세일즈 사용자 agent_sales
→ Bearer 토큰으로 사용자 식별
→ SALES_KNOWLEDGE_ROLE의 ALLOW TAG SALES 확인
→ 벡터 청크의 TECH_TAG 컬럼 값 검사
→ TECH_TAG에 SALES가 포함된 지식만 검색 결과에 포함
```
문서 청킹·임베딩 단계에서 청크마다 `TECH_TAG` 값을 저장한다. 이 값은 단순한 화면용 태그가 아니라, DDS가 보호 VIEW의 행을 판단하는 업무 분류값이다. 따라서 `agent_sales` 토큰으로 검색하면 세일즈 파이프라인 가이드처럼 `TECH_TAG=SALES`인 청크만 반환되고 HR·FINANCE 등 다른 값의 청크는 벡터 유사도가 높아도 반환되지 않는다.
## 인스턴스 경계 ## 인스턴스 경계
| 구분 | VPD Demo | DDS Demo | | 구분 | VPD Demo | DDS Demo |
@@ -23,7 +37,7 @@ VPD와 Oracle Deep Data Security(DDS)를 별도 인스턴스로 운영하면서
## 동일하게 비교할 수 있는 것 ## 동일하게 비교할 수 있는 것
- 사용자별 원본 접근 범위 - 업무 분류값별 지식 접근 범위
- 행 단위 허용/차단 - 행 단위 허용/차단
- 권한 없는 사용자의 default deny - 권한 없는 사용자의 default deny
- 보호 VIEW 우회 시도 차단 - 보호 VIEW 우회 시도 차단
@@ -75,6 +89,7 @@ WHERE EXISTS (허용된 TAG 중 하나가 청크 TAG와 일치)
| `agent_hr` | `SPRING_BOOT` OR `ORACLE_VPD` | `28001`, `28002` (2건) | | `agent_hr` | `SPRING_BOOT` OR `ORACLE_VPD` | `28001`, `28002` (2건) |
| `agent_fin_self` | `ORACLE_VPD` | `28002` (1건) | | `agent_fin_self` | `ORACLE_VPD` | `28002` (1건) |
| `agent_all` | 전체 허용 역할 | `28001`, `28002`, `28003` (3건) | | `agent_all` | 전체 허용 역할 | `28001`, `28002`, `28003` (3건) |
| `agent_sales` | `SALES` | `28004` (1건) |
토큰 행, 임시 함수, 임시 권한은 검증 직후 삭제했다. 이 결과는 “DDS 사용자 수 = 고객 사용자 수”가 아니라 “DDS 기술 사용자 1개 + 고객 권한 테이블” 모델이 동작함을 보여 준다. 토큰 행, 임시 함수, 임시 권한은 검증 직후 삭제했다. 이 결과는 “DDS 사용자 수 = 고객 사용자 수”가 아니라 “DDS 기술 사용자 1개 + 고객 권한 테이블” 모델이 동작함을 보여 준다.

View File

@@ -0,0 +1,173 @@
-- ============================================================
-- 36_dds_sales_knowledge_scenario.sql
-- Business scenario fixture: a sales employee searches only SALES-tagged
-- vector knowledge with a bearer token.
--
-- The access value is stored in CB_VECTOR_DOCUMENT_TAG.TECH_TAG. The
-- existing DDS token Data Grant (34_...) reads the common CB_* permission
-- tables at query time, so this script only adds the business user, role,
-- permission rule, sample chunk, and a short-lived demo token.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Adding the sales business user and role ===
MERGE INTO cb_app_user target
USING (
SELECT 104 user_id, 'agent_sales' user_name, 'E3001' employee_no,
'SALES' dept_code, 'N' can_read_contents, 'Y' active
FROM dual
) source
ON (target.user_id = source.user_id)
WHEN MATCHED THEN UPDATE SET
target.user_name = source.user_name,
target.employee_no = source.employee_no,
target.dept_code = source.dept_code,
target.can_read_contents = source.can_read_contents,
target.active = source.active
WHEN NOT MATCHED THEN INSERT (
user_id, user_name, employee_no, dept_code, can_read_contents, active
) VALUES (
source.user_id, source.user_name, source.employee_no,
source.dept_code, source.can_read_contents, source.active
);
MERGE INTO cb_app_role target
USING (
SELECT 40 role_id, 'SALES_KNOWLEDGE_ROLE' role_name,
'INTERNAL' max_sensitivity_level
FROM dual
) source
ON (target.role_id = source.role_id)
WHEN MATCHED THEN UPDATE SET
target.role_name = source.role_name,
target.max_sensitivity_level = source.max_sensitivity_level
WHEN NOT MATCHED THEN INSERT (
role_id, role_name, max_sensitivity_level
) VALUES (
source.role_id, source.role_name, source.max_sensitivity_level
);
MERGE INTO cb_user_role target
USING (SELECT 104 user_id, 40 role_id FROM dual) source
ON (target.user_id = source.user_id AND target.role_id = source.role_id)
WHEN NOT MATCHED THEN INSERT (user_id, role_id)
VALUES (source.user_id, source.role_id);
PROMPT === 2. Registering SALES as the allowed knowledge value ===
MERGE INTO cb_permission target
USING (
SELECT 400 perm_id, 40 role_id, 'CB_VECTOR_SEARCH_DOCUMENTS' target_name,
'SELECT' action_name, 'ALLOW' permission_effect
FROM dual
) source
ON (target.perm_id = source.perm_id)
WHEN MATCHED THEN UPDATE SET
target.role_id = source.role_id,
target.target_name = source.target_name,
target.action_name = source.action_name,
target.permission_effect = source.permission_effect
WHEN NOT MATCHED THEN INSERT (
perm_id, role_id, target_name, action_name, permission_effect
) VALUES (
source.perm_id, source.role_id, source.target_name,
source.action_name, source.permission_effect
);
MERGE INTO cb_permission_rule target
USING (
SELECT 4000 rule_id, 400 perm_id, 'TECH_TAG' rule_column,
'TAG' rule_type, 'SALES' rule_value
FROM dual
) source
ON (target.rule_id = source.rule_id)
WHEN MATCHED THEN UPDATE SET
target.perm_id = source.perm_id,
target.rule_column = source.rule_column,
target.rule_type = source.rule_type,
target.rule_value = source.rule_value
WHEN NOT MATCHED THEN INSERT (
rule_id, perm_id, rule_column, rule_type, rule_value
) VALUES (
source.rule_id, source.perm_id, source.rule_column,
source.rule_type, source.rule_value
);
PROMPT === 3. Adding a SALES-tagged vector knowledge chunk ===
MERGE INTO cb_vector_document_chunk target
USING (
SELECT 28004 chunk_id, 'knowledge-sales-001' document_id, 1 chunk_no,
'세일즈 파이프라인 운영 가이드' title,
TO_CLOB('세일즈 파이프라인 단계별 관리 기준과 영업 담당자용 고객 후속 조치 가이드입니다.') chunk_text,
'kb://business/sales/pipeline' source_uri,
TO_VECTOR('[0.65,0.25,0.15,0.10]') embedding
FROM dual
) source
ON (target.chunk_id = source.chunk_id)
WHEN MATCHED THEN UPDATE SET
target.document_id = source.document_id,
target.chunk_no = source.chunk_no,
target.title = source.title,
target.chunk_text = source.chunk_text,
target.source_uri = source.source_uri,
target.embedding = source.embedding
WHEN NOT MATCHED THEN INSERT (
chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding
) VALUES (
source.chunk_id, source.document_id, source.chunk_no, source.title,
source.chunk_text, source.source_uri, source.embedding
);
MERGE INTO cb_vector_document_tag target
USING (SELECT 28004 chunk_id, 'SALES' tech_tag FROM dual) source
ON (target.chunk_id = source.chunk_id AND target.tech_tag = source.tech_tag)
WHEN NOT MATCHED THEN INSERT (chunk_id, tech_tag)
VALUES (source.chunk_id, source.tech_tag);
MERGE INTO cb_vector_document_tag target
USING (SELECT 28004 chunk_id, 'INTERNAL' tech_tag FROM dual) source
ON (target.chunk_id = source.chunk_id AND target.tech_tag = source.tech_tag)
WHEN NOT MATCHED THEN INSERT (chunk_id, tech_tag)
VALUES (source.chunk_id, source.tech_tag);
PROMPT === 4. Issuing a short-lived demo token ===
DECLARE
v_key_id NUMBER;
BEGIN
UPDATE cb_agent_bearer_key
SET user_id = 104,
key_hash = STANDARD_HASH('dds_sales_demo_token', 'SHA256'),
active = 'Y',
revoked_at = NULL,
expires_at = SYSDATE + (1 / 24)
WHERE key_prefix = 'sales_demo';
IF SQL%ROWCOUNT = 0 THEN
SELECT NVL(MAX(key_id), 0) + 1 INTO v_key_id FROM cb_agent_bearer_key;
INSERT INTO cb_agent_bearer_key (
key_id, user_id, key_hash, key_prefix, issued_at,
expires_at, revoked_at, active
) VALUES (
v_key_id, 104, STANDARD_HASH('dds_sales_demo_token', 'SHA256'),
'sales_demo', SYSDATE, SYSDATE + (1 / 24), NULL, 'Y'
);
END IF;
END;
/
COMMIT;
SELECT u.user_name, u.dept_code, r.role_name,
p.target_name, pr.rule_column, pr.rule_value,
'dds_sales_demo_token' AS demo_token
FROM cb_app_user u
JOIN cb_user_role ur ON ur.user_id = u.user_id
JOIN cb_app_role r ON r.role_id = ur.role_id
JOIN cb_permission p ON p.role_id = r.role_id
JOIN cb_permission_rule pr ON pr.perm_id = p.perm_id
WHERE u.user_id = 104;
PROMPT Sales scenario ready: use dds_sales_demo_token in the DDS token search.
EXIT;

View File

@@ -0,0 +1,18 @@
-- Remove only the SALES knowledge scenario fixture.
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
DELETE FROM cb_agent_bearer_key WHERE key_prefix = 'sales_demo';
DELETE FROM cb_vector_document_tag WHERE chunk_id = 28004;
DELETE FROM cb_vector_document_chunk WHERE chunk_id = 28004;
DELETE FROM cb_permission_rule WHERE rule_id = 4000;
DELETE FROM cb_permission WHERE perm_id = 400;
DELETE FROM cb_user_role WHERE user_id = 104 AND role_id = 40;
DELETE FROM cb_app_role WHERE role_id = 40;
DELETE FROM cb_app_user WHERE user_id = 104;
COMMIT;
PROMPT Sales knowledge scenario removed.
EXIT;