[Developer] #573 normalize vector access whitelist

This commit is contained in:
devmrko
2026-06-30 16:33:59 +09:00
parent a9c58fa854
commit e0fbf1e0c5
8 changed files with 234 additions and 43 deletions

View File

@@ -268,15 +268,20 @@ public class OrdsProbeService {
}
static String vectorEffectiveSql(ProtectedObject object, String predicate) {
String vpdFilter = isAllowAllPredicate(predicate) ? "" : " AND (" + predicate + ")";
return "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(:embedding), COSINE) AS score"
+ " FROM " + object.owner() + "." + object.objectName() + " d"
+ " WHERE d.embedding IS NOT NULL AND (" + predicate + ")"
+ " WHERE d.embedding IS NOT NULL" + vpdFilter
+ " ORDER BY score) ranked_chunks"
+ " WHERE ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 10), 1), 100)";
}
private static boolean isAllowAllPredicate(String predicate) {
return predicate != null && predicate.trim().replaceAll("\\s+", " ").equals("1 = 1");
}
private String findVpdPredicate(String bearerToken, ProtectedObject object) {
try {
return jdbcTemplate.execute((ConnectionCallback<String>) connection -> {

View File

@@ -145,12 +145,13 @@
<pre>VECTOR_DISTANCE(d.embedding, TO_VECTOR(:embedding), COSINE) AS score
ORDER BY score ASC
ROWNUM &lt;= :row_limit</pre>
<p class="form-hint mb-0">이 부분이 검색어 벡터와 저장 벡터의 거리 계산입니다. 아래 권한 필터의 TAG 조건은 검색어 유사도 계산이 아니라 접근 통제입니다.</p>
<p class="form-hint mb-0">이 부분이 검색어 벡터와 저장 벡터의 거리 계산입니다. 아래에는 실제로 등록된 역할 기반 행 접근 조건만 표시됩니다.</p>
</section>
<section class="probe-exchange" data-sql-trace-field="vpd_predicate">
<h3 th:text="${vectorSearch ? '역할 기반 권한 필터 (VPD)' : 'VPD가 추가한 WHERE 조건'}">VPD가 추가한 WHERE 조건</h3>
<pre th:text="${result.vpdPredicate()}">(DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))</pre>
<p class="form-hint mb-0" th:if="${vectorSearch}">선택한 사용자의 직접 역할·그룹 상속 역할에 연결된 permission rule에서 계산된 조건입니다. 권한이 없으면 <code>1 = 0</code>, ALL이면 <code>1 = 1</code>이 됩니다.</p>
<pre th:if="${result.vpdPredicate() == '1 = 1'}">1 = 1 (ALL: 추가 행 필터 없음)</pre>
<pre th:unless="${result.vpdPredicate() == '1 = 1'}" th:text="${result.vpdPredicate()}">(DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))</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>

View File

@@ -23,4 +23,16 @@ class OrdsProbeServiceTest {
.contains("ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 10), 1), 100)")
.doesNotContain("Bearer", "0.10", "0.20");
}
@Test
void omitsNoopAllowAllPredicateFromVectorEffectiveSql() {
ProtectedObject object = new ProtectedObject(
5L, "ADMIN", "CB_VECTOR_SEARCH_DOCUMENTS", "cb-agent-vector/search", "Y");
String sql = OrdsProbeService.vectorEffectiveSql(object, " 1 = 1 ");
assertThat(sql)
.contains("WHERE d.embedding IS NOT NULL ORDER BY score")
.doesNotContain("AND (1 = 1)", "REGEXP_LIKE");
}
}

View File

@@ -147,10 +147,26 @@ class GuidedFlowTemplateTest {
assertThat(result)
.contains("토큰 적용 후 SQL")
.contains("권한 적용 SQL")
.contains("VPD가 추가한 WHERE 조건");
.contains("VPD가 추가한 WHERE 조건")
.contains("ALL: 추가 행 필터 없음")
.contains("기본 whitelist 역할");
assertThat(vectorSql)
.contains("VECTOR_DISTANCE(d.embedding, TO_VECTOR(:embedding), COSINE)")
.contains("WHERE d.embedding IS NOT NULL AND (' || v_vpd_predicate || ')");
.contains("IF TRIM(v_vpd_predicate) <> '1 = 1' THEN")
.contains("' AND (' || v_vpd_predicate || ')'");
}
@Test
void vectorDefaultRolesAreSeededAsWhitelistAllRules() throws IOException {
String setup = Files.readString(Path.of("sql/adb/32_dds_vector_tag_setup.sql"));
assertThat(setup)
.contains("ensure_vector_all_permission(10)")
.contains("ensure_vector_all_permission(20)")
.contains("ensure_vector_all_permission(30)")
.contains("UPPER(TRIM(rule_type)) = 'TAG'")
.contains("NULL, 'ALL', NULL")
.doesNotContain("SYS.ODCIVARCHAR2LIST('SPRING_BOOT', 'ORDS')");
}
@Test