feat: enforce DDS token access through common grants

This commit is contained in:
devmrko
2026-06-30 13:20:00 +09:00
parent 3a07788e5c
commit a5e70bcff1
19 changed files with 509 additions and 68 deletions

View File

@@ -15,7 +15,8 @@ public record DdsProperties(
String myObject,
String vectorObject,
Map<String, User> users,
Map<String, String> objectMappings
Map<String, String> objectMappings,
Token token
) {
private static final Pattern OBJECT_NAME = Pattern.compile(
@@ -47,6 +48,7 @@ public record DdsProperties(
});
}
objectMappings = Map.copyOf(normalizedMappings);
token = token == null ? new Token("dds_demo_token", "") : token;
}
/**
@@ -63,7 +65,8 @@ public record DdsProperties(
) {
this(dbUrl, queryTimeout, pgObject, myObject,
"ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS", users, Map.of(
"CB_VECTOR_SEARCH_DOCUMENTS", "ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS"));
"CB_VECTOR_SEARCH_DOCUMENTS", "ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS"),
new Token("dds_demo_token", ""));
}
public DdsProperties(
@@ -74,7 +77,15 @@ public record DdsProperties(
String vectorObject,
Map<String, User> users
) {
this(dbUrl, queryTimeout, pgObject, myObject, vectorObject, users, Map.of());
this(dbUrl, queryTimeout, pgObject, myObject, vectorObject, users, Map.of(),
new Token("dds_demo_token", ""));
}
public record Token(String username, String password) {
public boolean configured() {
return username != null && !username.isBlank()
&& password != null && !password.isBlank();
}
}
public String objectFor(String sourceKey) {

View File

@@ -18,11 +18,16 @@ import org.springframework.stereotype.Service;
/**
* Compiles the application's effective permission model into DDS grants.
*
* DDS does not evaluate CB_PERMISSION at query time. This service is the
* explicit publish boundary: application users/groups/roles are expanded,
* ALLOW/DENY rules are compiled into one predicate per user/object, and the
* resulting DATA GRANT is created or replaced. A publish with no ALLOW rule
* drops the reserved grant, preserving default deny.
* This service is the explicit publish boundary for the direct END USER
* comparison path: application users/groups/roles are expanded, ALLOW/DENY
* rules are compiled into one predicate per user/object, and the resulting
* DATA GRANT is created or replaced. A publish with no ALLOW rule drops the
* reserved grant, preserving default deny.
*
* The token-driven vector path is intentionally separate. Its object-level
* DATA GRANT calls a definer-rights predicate function that evaluates the
* common CB_* tables at query time after CB_AGENT_CTX has been initialized by
* the bearer token.
*/
@Service
public class DdsGrantPublisher {

View File

@@ -27,9 +27,11 @@ import org.springframework.stereotype.Service;
/**
* DDS version of the common knowledge flow.
*
* Ingestion still writes the shared chunk/tag store. Search deliberately does
* not call the VPD ORDS probe: it opens a DDS END USER connection and lets the
* DATA ROLE/DATA GRANT on the DDS-only view decide which chunks are visible.
* Ingestion still writes the shared chunk/tag store. Direct comparison search
* opens a DDS END USER connection and lets DATA ROLE/DATA GRANT decide which
* chunks are visible. Token search uses one configured technical DDS user,
* initializes CB_AGENT_CTX from the bearer token, and relies on the
* object-level DATA GRANT predicate to evaluate the common CB_* permissions.
*/
@Service
public class DdsVectorKnowledgeService {
@@ -144,6 +146,100 @@ public class DdsVectorKnowledgeService {
}
}
/**
* Search through the token-driven DDS path.
*
* The technical DDS end user is fixed in configuration. The bearer token is
* resolved by the shared CB_AGENT_CTX package on that same connection; the
* object-level DATA GRANT then evaluates the common CB_* permission model.
*/
public DdsVectorSearchResult searchByToken(
String bearerToken, String query, int requestedLimit, String embeddingMode) {
String normalizedToken = required(bearerToken, "Bearer 토큰");
if (normalizedToken.length() > 4096) {
throw new AppException("Bearer 토큰 길이가 허용 범위를 초과했습니다.");
}
String normalizedQuery = required(query, "검색 질문");
String mode = normalizeMode(embeddingMode);
var tokenUser = properties.token();
String userLabel = "토큰 기반 업무 사용자";
if (tokenUser == null || !tokenUser.configured()) {
return failure("token", userLabel, normalizedQuery, mode,
"DDS 토큰 기술 사용자가 설정되지 않았습니다.",
"DDS_BACKOFFICE_TOKEN_USERNAME/PASSWORD와 34번 DDS 설정을 확인하세요.", null);
}
if (properties.dbUrl().isBlank()) {
return failure("token", userLabel, normalizedQuery, mode,
"DDS 데이터베이스 연결 정보가 없습니다.",
"DDS_BACKOFFICE_DB_URL 또는 BACKOFFICE_DB_URL을 설정하세요.", null);
}
int limit = Math.max(1, Math.min(requestedLimit, 100));
int timeoutSeconds = timeoutSeconds(properties.queryTimeout());
String vectorJson = json(embed(normalizedQuery, mode));
Properties connectionProperties = new Properties();
connectionProperties.setProperty("user", jdbcUsername(tokenUser.username()));
connectionProperties.setProperty("password", tokenUser.password());
connectionProperties.setProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(timeoutSeconds * 1000));
connectionProperties.setProperty("oracle.jdbc.ReadTimeout", String.valueOf(timeoutSeconds * 1000));
String sql = "SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
+ "FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, d.source_uri, "
+ "d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(?), COSINE) AS score "
+ "FROM " + properties.vectorObject() + " d "
+ "WHERE d.embedding IS NOT NULL ORDER BY score) ranked_chunks WHERE ROWNUM <= ?";
try (Connection connection = java.sql.DriverManager.getConnection(properties.dbUrl(), connectionProperties)) {
connection.setReadOnly(true);
try (PreparedStatement context = connection.prepareStatement(
"BEGIN ADMIN.CB_AGENT_CTX_PKG.SET_USER_BY_BEARER(?); END;")) {
context.setString(1, normalizedToken);
context.execute();
}
String sessionUser = readSingleValue(connection,
"SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') FROM dual", timeoutSeconds);
String endUser = readSingleValue(connection,
"SELECT ORA_END_USER_CONTEXT.username FROM dual", timeoutSeconds);
String resolvedAppUser = readSingleValue(connection,
"SELECT user_name FROM ADMIN.CB_APP_USER "
+ "WHERE user_id = TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'))",
timeoutSeconds);
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setQueryTimeout(timeoutSeconds);
statement.setString(1, vectorJson);
statement.setInt(2, limit);
List<Map<String, Object>> rows = new ArrayList<>();
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("CHUNK_ID", resultSet.getLong("chunk_id"));
row.put("DOCUMENT_ID", resultSet.getString("document_id"));
row.put("CHUNK_NO", resultSet.getInt("chunk_no"));
row.put("TITLE", resultSet.getString("title"));
row.put("CHUNK_TEXT", resultSet.getString("chunk_text"));
row.put("SOURCE_URI", resultSet.getString("source_uri"));
row.put("TECH_TAG", resultSet.getString("tech_tag"));
row.put("SCORE", resultSet.getObject("score"));
rows.add(row);
}
}
String message = rows.isEmpty()
? "토큰으로 식별된 업무 사용자의 권한을 통과한 검색 단위가 없습니다."
: rows.size() + "개 검색 단위가 토큰으로 식별된 업무 사용자("
+ (resolvedAppUser == null ? "확인 불가" : resolvedAppUser)
+ ")의 DDS DATA GRANT를 통과했습니다.";
return new DdsVectorSearchResult(
"token", userLabel, properties.vectorObject(), normalizedQuery, mode,
embeddingModel(mode), true, "토큰 기반 DDS 권한으로 검색했습니다.", message,
sessionUser, endUser, null, rows);
}
} catch (SQLException exception) {
return failure("token", userLabel, normalizedQuery, mode,
classifyTitle(exception), classifyMessage(exception), oracleCode(exception));
}
}
private DdsVectorSearchResult failure(
String userKey,
String userLabel,
@@ -290,6 +386,9 @@ public class DdsVectorKnowledgeService {
private static String classifyTitle(SQLException exception) {
String message = flattenedMessage(exception).toUpperCase(Locale.ROOT);
if (message.contains("ORA-20002")) {
return "Bearer 토큰이 유효하지 않습니다.";
}
if (message.contains("ORA-00942")) {
return "이 DDS END USER에게 지식자료가 허용되지 않았습니다.";
}
@@ -306,6 +405,9 @@ public class DdsVectorKnowledgeService {
private static String classifyMessage(SQLException exception) {
String message = flattenedMessage(exception);
String upper = message.toUpperCase(Locale.ROOT);
if (upper.contains("ORA-20002")) {
return "토큰이 없거나 만료·회수되었거나 공통 사용자 매핑을 찾지 못했습니다.";
}
if (upper.contains("ORA-00942")) {
return "END USER의 DATA ROLE에 이 DDS 벡터 VIEW를 대상으로 한 DATA GRANT가 없습니다. default deny 결과입니다.";
}

View File

@@ -72,6 +72,23 @@ public class DdsVectorKnowledgeController {
return "fragments/dds-vector-search-result :: result";
}
@PostMapping("/vector-knowledge/token-search")
public String tokenSearch(
@RequestParam String bearerToken,
@RequestParam String query,
@RequestParam(defaultValue = "10") int limit,
@RequestParam(defaultValue = "DEMO") String embeddingMode,
Model model
) {
try {
model.addAttribute("searchResult",
service.searchByToken(bearerToken, query, limit, embeddingMode));
} catch (AppException | IllegalArgumentException exception) {
model.addAttribute("errorMessage", exception.getMessage());
}
return "fragments/dds-vector-search-result :: result";
}
private void populatePage(Model model) {
try {
model.addAttribute("summary", service.summary());

View File

@@ -64,6 +64,9 @@ dds:
object-mappings:
CB_VECTOR_SEARCH_DOCUMENTS: ${DDS_BACKOFFICE_VECTOR_OBJECT:ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS}
CB_V_SEARCH_DOCUMENTS: ${DDS_BACKOFFICE_DOCUMENT_OBJECT:ADMIN.CB_DDS_V_SEARCH_DOCUMENTS}
token:
username: ${DDS_BACKOFFICE_TOKEN_USERNAME:dds_demo_token}
password: ${DDS_BACKOFFICE_TOKEN_PASSWORD:${DDSUSER_TOKEN_PASSWORD:}}
users:
my:
label: MY 전용 사용자

View File

@@ -7,10 +7,10 @@
<header class="page-title guided-hero">
<span class="architecture-kicker">DEEP DATA SECURITY · INDEPENDENT TRACK</span>
<h1>같은 권한 기준을 DDS 방식으로 적용하고 확인합니다.</h1>
<p class="context-summary">권한 설계 → DDS 보호 연결 → 권한 반영 → END USER 검증 → 지식 검색 결과 확인</p>
<p class="context-summary">권한 설계 → 객체별 DATA GRANT → 토큰 Context 또는 END USER → 보호 결과 확인</p>
<details class="explanation-details">
<summary>이 인스턴스의 역할 보기</summary>
<p>8083 DDS 인스턴스는 8082 VPD 인스턴스와 별도로 실행됩니다. 두 화면은 사용자·그룹·역할·권한 규칙을 같은 관리 흐름으로 보여주지만, DDS는 END USER·DATA ROLE·DATA GRANT를 통해 데이터를 보호합니다.</p>
<p>8083 DDS 인스턴스는 8082 VPD 인스턴스와 별도로 실행됩니다. 두 화면은 사용자·그룹·역할·권한 규칙을 같은 관리 흐름으로 보여주, DDS는 보호 객체별 DATA GRANT로 그 규칙을 집행합니다.</p>
<p class="mb-0">일상적인 권한 변경은 권한 관리에서 하고, DDS 보호 객체와 실제 결과는 이 인스턴스에서 확인합니다.</p>
</details>
</header>
@@ -20,14 +20,14 @@
<section class="journey-grid" aria-label="DDS 권한 적용 네 단계">
<a class="journey-card" href="/permissions"><span class="journey-number">1</span><div><h2>1. 권한 설계</h2><p>사용자·그룹·역할에 객체와 TAG 규칙을 연결합니다.</p><strong>권한 규칙 만들기 →</strong></div></a>
<a class="journey-card" href="/vpd-policies"><span class="journey-number">2</span><div><h2>2. DDS 보호 연결</h2><p>공통 규칙을 DDS 전용 VIEW와 DATA GRANT에 연결합니다.</p><strong>DDS 보호 객체 확인 →</strong></div></a>
<a class="journey-card" href="/dds-provision"><span class="journey-number">3</span><div><h2>3. DDS 권한 반영</h2><p>그룹 상속을 포함한 유효 권한을 DDS Grant로 게시합니다.</p><strong>Grant 미리보기·게시 →</strong></div></a>
<a class="journey-card" href="/dds"><span class="journey-number">4</span><div><h2>4. END USER</h2><p>실제 DDS END USER로 접속해 허용·차단 결과를 확인합니다.</p><strong>직접 조회 실행</strong></div></a>
<a class="journey-card" href="/vector-knowledge"><span class="journey-number">5</span><div><h2>5. 지식 검색</h2><p>청크·임베딩·태그와 DDS 권한을 결합한 검색 시나리오를 확인합니다.</p><strong>권한 기반 검색</strong></div></a>
<a class="journey-card" href="/dds-provision"><span class="journey-number">3</span><div><h2>3. DDS 권한 연결</h2><p>보호 VIEW별 DATA GRANT와 공통 권한 predicate를 확인합니다.</p><strong>Grant 미리보기·게시 →</strong></div></a>
<a class="journey-card" href="/vector-knowledge"><span class="journey-number">4</span><div><h2>4. 토큰 권한</h2><p>하나의 기술 사용자에 Bearer 토큰을 연결해 공통 권한을 적용합니다.</p><strong>토큰 기반 검색</strong></div></a>
<a class="journey-card" href="/dds"><span class="journey-number">5</span><div><h2>5. 직접 END USER 비교</h2><p>저수준 DDS END USER·DATA ROLE 결과를 별도로 확인합니다.</p><strong>직접 조회 실행</strong></div></a>
</section>
<section class="content-band macro-micro-grid">
<div><span class="architecture-kicker">MACRO · 전체 관점</span><h2>관리 기준은 하나입니다.</h2><p>누가 어떤 지식자료를 볼 수 있는지는 사용자·그룹·역할·권한 규칙에서 설명합니다. VPD와 DDS는 같은 기준을 서로 다른 DB 보호 방식으로 집행합니다.</p></div>
<div><span class="architecture-kicker">MICRO · 실행 관점</span><h2>DDS가 선언형 권한을 집행합니다.</h2><p><code>END USER → DATA ROLE → DATA GRANT → 보호 VIEW</code>가 연결되지 않으면 객체가 보이지 않습니다. 권한이 없을 때 0건이 아니라 <code>ORA-00942</code>가 될 수 있습니다.</p></div>
<div><span class="architecture-kicker">MICRO · 실행 관점</span><h2>객체별 DATA GRANT가 최종 집행합니다.</h2><p>토큰 경로는 <code>Bearer → CB_AGENT_CTX → DATA GRANT predicate → 보호 VIEW</code>, 직접 비교는 <code>END USER → DATA ROLE → DATA GRANT → 보호 VIEW</code>입니다. 권한이 없으면 객체가 보이지 않 <code>ORA-00942</code>가 될 수 있습니다.</p></div>
</section>
<section class="summary-grid" aria-label="현재 DDS 관리 현황">
@@ -40,7 +40,7 @@
<div class="section-heading"><div><h2>현재 DDS 검증 대상</h2><p class="section-subtitle">고객 데이터 원본과 벡터 지식자료를 각각 DDS 전용 객체로 확인합니다.</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>
<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>TAG DATA GRANT + 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>
</tbody></table></div>
</section>
</main>

View File

@@ -10,8 +10,8 @@
<p class="context-summary">사용자·그룹·역할·테이블 권한을 계산한 뒤 DDS DATA GRANT로 게시합니다.</p>
<details class="explanation-details">
<summary>VPD와 DDS의 반영 차이 보기</summary>
<p>VPD는 요청 때마다 권한 테이블을 읽습니다. DDS는 같은 권한체계를 배포 시점에 DATA GRANT로 컴파일합니다. 그래서 권한 저장 후 이 화면에서 변경 내용을 미리 확인하고 게시해야 합니다.</p>
<p class="mb-0">그룹 자체를 DDS 그룹으로 복사하지는 않습니다. 애플리케이션 그룹의 역할 상속을 계산해 매핑된 DDS DATA ROLE에 하나의 최종 predicate로 합칩니다.</p>
<p>이 화면은 직접 DDS END USER 비교 경로와 일반 객체의 선언형 Grant를 미리 보고 게시하는 단계입니다. 토큰 기반 벡터 경로는 별도 객체별 Data Grant predicate가 요청 시 공통 권한 테이블을 다시 평가합니다.</p>
<p class="mb-0">그룹 자체를 DDS 그룹으로 복사하지는 않습니다. 애플리케이션 그룹의 역할 상속을 계산해 직접 비교용 DATA ROLE Grant에 반영하고, 토큰 경로에서는 같은 effective role 계산을 predicate 함수가 사용합니다.</p>
</details>
</div>
@@ -27,6 +27,7 @@
<tr><td>애플리케이션 사용자</td><td>매핑된 DDS END USER + DATA ROLE</td><td>게시할 때 매핑 확인</td></tr>
<tr><td>그룹과 그룹에 연결된 역할</td><td>그룹을 복사하지 않고 최종 predicate로 합산</td><td>미리보기·게시 때 계산</td></tr>
<tr><td>테이블·VIEW SELECT 권한</td><td>보호 객체별 <code>DATA GRANT ... WHERE ...</code></td><td>권한 변경 후 게시</td></tr>
<tr><td>Bearer 토큰 벡터 검색</td><td>기술 사용자 Context + 객체별 Data Grant predicate</td><td><code>34_dds_token_data_grant_common_auth.sql</code> 경계</td></tr>
<tr><td>원문 표시 허용 컬럼</td><td><code>AS SELECT</code> 또는 <code>ALL COLUMNS EXCEPT</code></td><td>권한 변경 후 게시</td></tr>
</tbody>
</table>

View File

@@ -69,7 +69,7 @@
<div th:fragment="trackNotice" th:if="${backofficeTrack == 'DDS'}" class="container pt-3">
<div class="alert alert-info mb-0">
<strong>DDS 독립 데모</strong> · VPD와 같은 관리 흐름을 사용하지만, 실제 보호 적용은 <code>END USER → DATA ROLE → DATA GRANT</code>로 별도 처리합니다.
<strong>DDS 독립 데모</strong> · VPD와 같은 관리 흐름을 사용하, 실제 보호는 객체별 <code>DATA GRANT</code>가 담당합니다. 토큰 경로는 <code>Bearer → CB_AGENT_CTX → predicate</code>, 직접 비교는 <code>END USER → DATA ROLE → DATA GRANT</code>니다.
</div>
</div>
@@ -83,13 +83,13 @@
<div class="architecture-step" th:classappend="${activeLayer == 'vpd'} ? ' active'">
<span class="architecture-kicker" th:text="${backofficeTrack == 'DDS' ? '2 · ENFORCE' : '2 · ENFORCE'}">2 · ENFORCE</span>
<strong th:text="${backofficeTrack == 'DDS' ? 'DDS 보호 연결' : 'DB 보호 연결'}">DB 보호 연결</strong>
<p th:text="${backofficeTrack == 'DDS' ? 'DATA ROLE과 DATA GRANT가 관리된 권한을 보호 객체에 선언합니다.' : 'VPD가 저장된 권한체계를 매번 읽어 DB에서 행을 자동 제한합니다.'}">DB 보호 정책이 권한을 적용합니다.</p>
<p th:text="${backofficeTrack == 'DDS' ? '보호 객체별 DATA GRANT predicate가 관리된 권한을 행·컬럼에 적용합니다.' : 'VPD가 저장된 권한체계를 매번 읽어 DB에서 행을 자동 제한합니다.'}">DB 보호 정책이 권한을 적용합니다.</p>
</div>
<div class="architecture-arrow"></div>
<div class="architecture-step" th:classappend="${activeLayer == 'token'} ? ' active'">
<span class="architecture-kicker">3 · IDENTITY</span>
<strong th:text="${backofficeTrack == 'DDS' ? 'END USER 컨텍스트' : '검증 세션'}">검증 세션</strong>
<p th:text="${backofficeTrack == 'DDS' ? 'DDS END USER 또는 지원 드라이버 컨텍스트로 조회 주체를 전달합니다.' : '확인할 사용자를 나타내는 일회성 토큰을 준비합니다.'}">조회 주체를 준비합니다.</p>
<p th:text="${backofficeTrack == 'DDS' ? '토큰 Context 또는 DDS END USER로 조회 주체를 전달합니다.' : '확인할 사용자를 나타내는 일회성 토큰을 준비합니다.'}">조회 주체를 준비합니다.</p>
</div>
<div class="architecture-arrow"></div>
<div class="architecture-step" th:classappend="${activeLayer == 'ords'} ? ' active'">

View File

@@ -11,7 +11,7 @@
<details class="explanation-details">
<summary>이 화면의 큰 흐름 보기</summary>
<p>문서는 검색 가능한 청크로 나뉘고 각 청크에 기술 태그가 붙습니다. 권한 관리에서 역할별로 허용할 태그를 등록하면 DDS의 DATA GRANT가 해당 태그가 붙은 청크만 검색 대상으로 남깁니다.</p>
<p class="mb-0">VPD 화면과 같은 사용자·그룹·역할·권한 규칙을 사용하지만, 실행 시점에는 Bearer 기반 VPD predicate가 아니라 DDS END USER의 DATA ROLE과 DATA GRANT가 적용됩니다.</p>
<p class="mb-0">VPD 화면과 같은 사용자·그룹·역할·권한 규칙을 사용합니다. 아래에는 DDS END USER를 직접 선택하는 비교 경로와, 하나의 DDS 기술 사용자에 Bearer 토큰을 전달해 공통 권한을 평가하는 제품형 경로를 함께 둡니다.</p>
</details>
</div>
@@ -85,16 +85,58 @@
</div>
<details class="explanation-details mt-3">
<summary>VPD와 DDS의 차이 보기</summary>
<p>VPD는 요청마다 권한 테이블을 읽어 predicate를 계산합니다. DDS는 보호 VIEW에 선언한 DATA GRANT와 END USER 매핑을 DB가 적용합니다. 따라서 태그 규칙을 바꾸면 DDS grant 반영 절차도 함께 확인해야 합니다.</p>
<p>VPD는 요청마다 권한 테이블을 읽어 predicate를 계산합니다. DDS의 직접 비교 경로는 보호 VIEW에 선언한 DATA GRANT와 END USER 매핑을 용합니다. 토큰 경로에서는 토큰이 공통 사용자 Context를 만들고, 보호 VIEW의 객체별 DATA GRANT predicate가 같은 권한 테이블을 다시 평가합니다.</p>
</details>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">3 · SEARCH</span>
<h2>DDS 권한으로 지식 검색</h2>
<p class="section-subtitle">선택한 END USER로 직접 접속해 DATA ROLE/DATA GRANT가 통과시킨 청크만 반환합니다.</p>
<span class="architecture-kicker">3 · TOKEN CONTEXT</span>
<h2>Bearer 토큰으로 같은 DDS Data Grant 적용</h2>
<p class="section-subtitle">고객별 DDS 계정을 만들지 않고 하나의 기술 사용자 세션에서 토큰으로 업무 사용자를 식별합니다.</p>
</div>
<span class="badge text-bg-secondary">객체별 DATA GRANT</span>
</div>
<details class="explanation-details">
<summary>토큰 경계 설명 보기</summary>
<ol>
<li>서버가 설정한 하나의 DDS 기술 사용자로 연결합니다.</li>
<li>Bearer 토큰을 <code>CB_AGENT_CTX_PKG.SET_USER_BY_BEARER</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>
</ol>
<p class="mb-0">토큰은 DATA GRANT 문법의 바인드 파라미터가 아닙니다. 토큰이 만든 신뢰된 Context를 Data Grant predicate가 참조하는 구조입니다. 토큰은 저장하거나 화면에 재표시하지 않습니다.</p>
</details>
<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}">
<label class="span-2">Bearer 토큰
<input class="form-control" name="bearerToken" type="password" autocomplete="off" placeholder="발급된 임시 토큰을 붙여 넣으세요." required>
<span class="form-hint">요청 처리 후 저장하지 않습니다. 회수·만료된 토큰은 거부됩니다.</span>
</label>
<label>결과 수<input class="form-control" name="limit" type="number" min="1" max="100" value="10"></label>
<label>임베딩 방식
<select class="form-select" name="embeddingMode">
<option value="DEMO">로컬 임베딩(개발용)</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option>
</select>
</label>
<label class="span-2">검색 질문
<textarea class="form-control" name="query" rows="3" placeholder="예: ORACLE_VPD 태그가 있는 지식자료의 접근 조건" required></textarea>
</label>
<button class="btn rw-btn-primary" type="submit">토큰 권한으로 검색</button>
</form>
<section id="vector-token-search-result" class="mt-3" aria-live="polite">
<div class="empty-result-guide">Bearer 토큰과 질문을 입력하면 공통 권한체계를 반영한 DDS Data Grant 결과를 표시합니다.</div>
</section>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">4 · DIRECT CHECK</span>
<h2>DDS END USER 직접 조회 비교</h2>
<p class="section-subtitle">저수준 DDS 검증을 위해 선택한 END USER로 직접 접속해 DATA ROLE/DATA GRANT 결과를 확인합니다.</p>
</div>
<span class="badge text-bg-primary" th:text="${ddsVectorObject}">DDS view</span>
</div>

View File

@@ -10,7 +10,7 @@
<p class="context-summary">권한 관리에서 만든 규칙을 DDS DATA ROLE/DATA GRANT로 연결하고 실제 검색 결과로 확인합니다.</p>
<details class="explanation-details">
<summary>VPD의 Policy/Filter와 무엇이 다른가요?</summary>
<p>VPD는 Policy가 Filter function을 호출해 요청마다 predicate를 계산합니다. DDS그 두 단계를 별도 함수로 만들지 않고, 보호 VIEW와 DATA GRANT에 행·컬럼 조건을 선언합니다.</p>
<p>VPD는 Policy가 Filter function을 호출해 요청마다 predicate를 계산합니다. DDS는 보호 VIEW/TABLE별 DATA GRANT에 행·컬럼 조건을 선언하고, 필요하면 predicate가 공통 권한을 읽는 Definer-rights 함수를 호출합니다.</p>
<p class="mb-0">따라서 이 화면에서는 VPD Filter를 수정하지 않습니다. 일상적인 변경은 <a href="/permissions">권한 관리</a>에서 하고, DDS grant 반영은 승인된 SQL/배포 절차로 수행합니다.</p>
</details>
</div>
@@ -36,7 +36,7 @@
<span>사용자·그룹·역할</span><strong></strong>
<span>객체별 행·TAG·컬럼 권한</span><strong></strong>
<span>DATA ROLE / DATA GRANT</span><strong></strong>
<span>END USER 결과</span>
<span>토큰 Context 또는 END USER 결과</span>
</div>
<p class="text-muted mb-0">관리 대상 이름: <code th:text="${managementObject}">CB_VECTOR_SEARCH_DOCUMENTS</code></p>
</section>
@@ -55,8 +55,8 @@
<tbody>
<tr>
<td><code th:text="${ddsVectorObject}">ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS</code></td>
<td>END USER → DATA ROLE → DATA GRANT</td>
<td>DATA GRANT가 없으면 객체가 보이지 않음</td>
<td>토큰 Context/END USER → DATA ROLE → 객체별 DATA GRANT</td>
<td>Grant predicate가 없거나 조건 불일치 시 차단</td>
<td><a class="btn btn-sm btn-outline-primary" href="/vector-knowledge">검색 결과 확인</a></td>
</tr>
</tbody>
@@ -64,8 +64,8 @@
</div>
<details class="explanation-details mt-3">
<summary>추가·수정 가이드</summary>
<p>새 보호 대상을 추가할 때는 (1) 권한 관리에 객체와 TAG 규칙을 등록하고 (2) DDS 전용 VIEW 만들고 (3) 해당 DATA ROLE<code>CREATE OR REPLACE DATA GRANT ... WHERE ...</code>를 연결한 뒤 (4) 허용·거부·권한 없음 사용자를 직접 검증합니다.</p>
<p class="mb-0">현재 벡터 예제의 기준 SQL은 <code>sql/adb/32_dds_vector_tag_setup.sql</code>입니다. 이 예제는 임의의 한 테이블만 강제하는 유일한 방식이 아니라, 실제 업무 VIEW/Handler로 확장하기 위한 기준 흐름입니다.</p>
<p>새 보호 대상을 추가할 때는 (1) 권한 관리에 논리 객체와 TAG 규칙을 등록하고 (2) DDS 전용 VIEW/TABLE을 만들고 (3) 해당 객체<code>CREATE OR REPLACE DATA GRANT ... WHERE ...</code>를 연결하고 (4) 토큰 Context와 직접 END USER의 허용·거부·권한 없음을 각각 검증합니다.</p>
<p class="mb-0">벡터 기준 SQL은 <code>sql/adb/32_dds_vector_tag_setup.sql</code>, 토큰 기반 공통 predicate 기준은 <code>sql/adb/34_dds_token_data_grant_common_auth.sql</code>입니다.</p>
</details>
</section>