From 3a07788e5c3497e7d99bec7cafc1c39cb9552c04 Mon Sep 17 00:00:00 2001 From: devmrko Date: Tue, 30 Jun 2026 11:40:02 +0900 Subject: [PATCH] [Developer] #567 expose VPD effective SQL trace --- docs/design/567-ords-vpd-sql-trace/README.md | 137 ++++++++++++++++++ .../fn-addLocalSqlTrace.md | 73 ++++++++++ .../fn-findVpdPredicate.md | 73 ++++++++++ docs/reference/ords-vpd-sql-trace.md | 17 +++ ...17_agent_ords_security_local_vpd_setup.sql | 1 + ...agent_ords_security_ords_handler_setup.sql | 19 +++ ...agent_ords_security_dynamic_vpd_filter.sql | 5 + sql/adb/29_agent_ords_vector_search_ords.sql | 19 +++ sql/adb/33_agent_ords_sql_trace_grant.sql | 21 +++ .../domain/probe/ProbeResult.java | 56 ++++++- .../service/OrdsMetadataService.java | 34 ++++- .../service/OrdsProbeService.java | 99 ++++++++++++- .../templates/fragments/probe-result.html | 27 ++++ .../resources/templates/ords-handlers.html | 6 +- .../domain/probe/ProbeResultTest.java | 25 ++++ .../service/OrdsMetadataServiceTest.java | 54 +++++++ .../web/GuidedFlowTemplateTest.java | 14 ++ 17 files changed, 675 insertions(+), 5 deletions(-) create mode 100644 docs/design/567-ords-vpd-sql-trace/README.md create mode 100644 docs/design/567-ords-vpd-sql-trace/fn-addLocalSqlTrace.md create mode 100644 docs/design/567-ords-vpd-sql-trace/fn-findVpdPredicate.md create mode 100644 docs/reference/ords-vpd-sql-trace.md create mode 100644 sql/adb/33_agent_ords_sql_trace_grant.sql create mode 100644 src/test/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataServiceTest.java diff --git a/docs/design/567-ords-vpd-sql-trace/README.md b/docs/design/567-ords-vpd-sql-trace/README.md new file mode 100644 index 0000000..cdd0a3b --- /dev/null +++ b/docs/design/567-ords-vpd-sql-trace/README.md @@ -0,0 +1,137 @@ +# 설계서: ORDS/VPD 권한 적용 SQL trace (#567) + +> **상태**: Approved +> **작성**: [AI] Architect · **최종수정**: 2026-06-30 +> **추적성** — Redmine: #567 · 관련 이슈: #561 · 관련 ADR: 없음 +> · 구현 파일: `ProbeResult`, `OrdsProbeService`, `OrdsMetadataService`, `probe-result.html`, `ords-handlers.html`, `sql/adb/17_*`, `22_*`, `26_*`, `29_*`, `33_*` +> · 테스트: `ProbeResultTest`, `OrdsMetadataServiceTest`, `GuidedFlowTemplateTest`, `mvn test` + +## 1. 목적 (Why) + +권한 결과 확인에서 `set_vpd_context`가 토큰 사용자를 DB 컨텍스트로 설정한 뒤, VPD 정책 함수가 반환한 행 조건이 기본 SELECT에 어떻게 결합되는지 운영자가 직접 확인할 수 있게 한다. 이 기능은 #561의 Handler 흐름 설명을 실행 증거까지 확장한다. + +## 2. 범위 (Scope) + +- **포함**: + - ORDS Handler 성공 응답의 `vpd_predicate`, `effective_sql` trace 필드. + - 기존 trace 미적용 Handler를 위한 동일 DB fallback predicate 조회. + - `/probe` 결과의 “토큰 적용 후 SQL” 영역과 trace 미지원 안내. + - 신규/갱신 Handler SQL 및 기존 설치용 `GRANT` 패치 스크립트. + - 토큰 원문 마스킹과 trace 관련 단위/템플릿/소스 테스트. +- **제외 (out of scope)**: + - Oracle optimizer 실행계획, `V$SQL` 조회, bind 값 치환 결과의 수집. + - custom VPD Filter의 임의 SQL 해석 또는 자동 수정. + - 권한 모델/토큰 저장 구조 변경. + - 원문 Bearer Token의 응답·로그·DB 저장. + +## 3. 인수조건 (Acceptance Criteria) + +- [ ] 유효 토큰으로 권한 결과를 조회하면 VPD predicate와 기본 SELECT에 결합한 effective SQL을 확인할 수 있다. +- [ ] 0행 결과도 성공적인 VPD 차단 결과로 trace 표시 대상이 된다. +- [ ] 신규/갱신 object Handler와 기본 VPD/벡터 Handler는 진단 필드를 응답에 포함한다. +- [ ] 기존 Handler가 진단 필드를 반환하지 않아도 같은 DB의 `set_user_by_bearer` 컨텍스트에서 기본 VPD predicate를 재조회한다. +- [ ] trace를 얻을 수 없는 DB 계정·custom Filter·구버전 Handler는 조회 결과를 실패로 바꾸지 않고 적용 방법을 안내한다. +- [ ] trace에는 predicate 표현식과 SQL 구조만 포함하며 Bearer 원문은 포함하지 않는다. +- [ ] 초기화 및 기존 설치용 SQL에 `CB_AGENT_DOC_VPD_FILTER` 실행 권한과 갱신 절차가 명시된다. +- [ ] 관련 단위/템플릿/Handler source 테스트와 전체 Maven 테스트가 통과한다. + +## 4. 컨텍스트 & 제약 + +- ORDS는 공통 `CB_ORDS` 계정으로 접속하고 `cb_ords_handler_pkg.set_vpd_context`가 Bearer key를 `CB_AGENT_CTX`에 매핑한다. +- 기본 VPD 함수 `ADMIN.CB_AGENT_DOC_VPD_FILTER`가 현재 컨텍스트의 권한 규칙으로 predicate를 만든다. 정책 함수의 반환값을 직접 표시하는 것이 행 결과를 다시 계산하는 별도 Java 권한 로직보다 정합성이 높다. +- 기존 Handler는 응답 계약을 바꾸지 않을 수 있으므로, 백오피스 DB 연결에서 같은 Bearer key를 별도 세션 컨텍스트에 설정해 fallback을 수행한다. fallback은 세션 종료 전에 반드시 `clear_user`를 실행한다. +- ORDS 응답 trace가 없거나 fallback 권한이 없으면 권한 결과 자체는 그대로 반환한다. trace는 관측성 보조 기능이며 접근 통제 경로가 아니다. +- `ADMIN`은 현재 PoC의 VPD 함수/컨텍스트 소유자다. 다른 소유자 구조는 이번 범위에서 자동 추론하지 않는다. + +## 5. 아키텍처 개요 + +```text +[Probe form: bearerToken + object] + │ + ▼ + [ORDS Handler: set_vpd_context] + │ + ┌──────┴────────┐ + │ │ + ▼ ▼ + [DBMS_RLS SELECT] [same-context trace] + │ │ + └──────┬────────┘ + ▼ + [ORDS JSON: items + vpd_predicate + effective_sql] + │ + ▼ (trace absent only) + [Backoffice DB fallback: set_user_by_bearer → filter → clear_user] + │ + ▼ + [ProbeResult → “토큰 적용 후 SQL”] +``` + +- I/O 경계: `OrdsProbeService.findVpdPredicate`와 ORDS HTTP 호출은 외부 I/O다. `ProbeResult.hasSqlTrace/withSqlTrace`와 SQL 문자열 조합은 표현/변환 책임으로 둔다. +- 안전 경계: predicate 조회 실패는 조회 실패가 아니며 trace만 생략한다. Bearer header는 기존처럼 마스킹된 request detail만 노출한다. + +## 6. 데이터 모델 + +- `ProbeResult.vpdPredicate`: VPD 정책 함수가 현재 토큰 컨텍스트에서 반환한 predicate 문자열(nullable). +- `ProbeResult.effectiveSql`: Handler의 기본 SELECT와 predicate/row limit을 결합한 확인용 SQL(nullable). +- ORDS JSON 성공 응답에 두 문자열을 optional root field로 추가한다. `items`/`rows` 기존 배열 계약은 유지한다. +- trace SQL은 `:row_limit` bind 표기를 유지하며 실제 Bearer 값이나 context 값은 문자열에 치환하지 않는다. + +## 7. 함수 명세 (Function Specs) + +| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? | +|------|-----------|----------------|------|------|-----------|-------| +| `ProbeResult.hasSqlTrace` | effective SQL 존재 여부를 판단 | `boolean hasSqlTrace()` | result fields | boolean | blank/null이면 false | 단순 | +| `ProbeResult.withSqlTrace` | 기존 결과에 trace 두 필드를 불변으로 부착 | `ProbeResult withSqlTrace(String, String)` | predicate, SQL | 새 `ProbeResult` | 없음 | 단순 | +| `OrdsProbeService.runProbe` | ORDS 결과를 파싱하고 필요한 경우 trace fallback을 연결 | `ProbeResult runProbe(ProbeCommand)` | token/object/limit | ProbeResult | 기존 상태 분류 유지 | **복잡** | +| `parseSuccess` | ORDS JSON 배열과 optional trace 필드 파싱 | `ProbeResult parseSuccess(...)` | body, objectId, HTTP evidence | ProbeResult | 배열 누락 시 예외 | 단순 | +| `traceValue` | JSON scalar trace 필드 정규화 | `String traceValue(JsonNode, String)` | root/field | nullable String | 비 scalar/blank이면 null | 단순 | +| `addLocalSqlTrace` | predicate와 보호 객체 컬럼으로 effective SQL 생성 | `ProbeResult addLocalSqlTrace(...)` | result/token/object | trace 부착 결과 | DB/컬럼 조회 실패 시 원 결과 | **복잡** | +| `findVpdPredicate` | 같은 DB 연결에서 컨텍스트 설정→predicate 조회→컨텍스트 정리 | `String findVpdPredicate(String, ProtectedObject)` | bearer/object | nullable predicate | 권한/DB 오류는 null | **복잡** | +| `executeContextSetter` | DB 세션에 Bearer 기반 사용자 컨텍스트 설정 | `void executeContextSetter(Connection, String)` | connection/token | 없음 | SQLException | 단순 | +| `clearContext` | fallback 세션의 사용자 컨텍스트 제거 | `void clearContext(Connection)` | connection | 없음 | cleanup 오류 무시 | 단순 | +| `objectQuerySource` | 생성 Handler에 trace metadata 출력 블록을 포함 | `String objectQuerySource(ProtectedObject,List)` | object/columns | PL/SQL source | 식별자/컬럼 검증은 호출부 | **복잡** | + +복잡 함수 상세는 `fn-addLocalSqlTrace.md`, `fn-findVpdPredicate.md`에 둔다. `runProbe`의 기본 상태 분류 계약은 기존 #557 설계를 계승한다. + +## 8. 흐름 / 알고리즘 + +1. ORDS base URL, token, 보호 객체를 기존 검증 절차로 확인한다. +2. ORDS POST를 호출한다. 갱신된 Handler는 `set_vpd_context` 직후 동일 VPD 함수를 호출해 trace를 만들고, `items`와 함께 JSON으로 반환한다. +3. 백오피스는 `items`/`rows`를 기존 방식으로 파싱한다. +4. trace가 없고 일반 object Handler이면 백오피스 DB의 한 연결에서 `set_user_by_bearer`를 호출한다. +5. 같은 연결에서 `SELECT admin.cb_agent_doc_vpd_filter(?, ?) FROM dual`을 수행한다. +6. `finally` 경로에서 `clear_user`를 호출하고 연결을 pool에 반환한다. +7. 보호 객체의 등록 컬럼과 predicate를 이용해 `SELECT ... FROM OWNER.OBJECT o WHERE (...) AND ROWNUM ...` 형태를 만든다. +8. 화면은 결과 행·사용자 권한 뒤에 predicate와 effective SQL을 표시한다. trace가 없으면 ORDS/DB 적용 절차를 안내한다. + +## 9. 엣지케이스 & 에러 처리 + +- token/ORDS 오류: 기존 `ProbeStatus`와 HTTP evidence 처리만 수행하며 fallback을 실행하지 않는다. +- 0행: `VPD_DENY_EMPTY_RESULT`를 유지하고 trace를 표시한다. +- ORDS 신규 trace가 있고 local DB trace가 불가능한 경우: ORDS 응답 trace를 그대로 사용한다. +- 기존 Handler·DB 계정에 trace 권한이 없는 경우: 결과 성공/0행을 유지하고 trace 미지원 안내를 표시한다. +- fallback context setter가 실패한 경우: 반드시 cleanup을 시도하고 trace는 null로 둔다. +- vector object: 전용 Handler의 복합 vector SQL을 일반 `SELECT`로 오표시하지 않도록 local generic fallback을 생략한다. 갱신된 vector Handler가 자체 trace를 반환하면 표시한다. +- custom VPD Filter: 기본 함수 반환값을 custom Filter의 실제 predicate로 주장하지 않는다. trace가 없으면 별도 Filter 확인을 안내한다. +- predicate가 길어도 DB 함수 반환 한도(`VARCHAR2(32767)`)와 화면 `
`로 처리한다.
+
+## 10. 테스트 계획
+
+- `ProbeResultTest`: trace 부착 후 기존 rows/status 보존, blank trace 미표시.
+- `OrdsMetadataServiceTest`: 생성 object Handler에 `set_vpd_context`, default VPD function, JSON trace 필드와 컬럼 SQL이 포함되는지 확인.
+- `GuidedFlowTemplateTest`: 결과 화면/Handler 안내/초기화 SQL/기존 설치 grant 및 vector trace source 확인.
+- `mvn test`: 전체 단위·템플릿 테스트.
+- 운영 smoke: 신규 토큰으로 `/probe`에서 HR/SELF/ALL 결과를 각각 실행하고 predicate의 `DEPT_CODE`, `OWNER_EMP_NO`, `1 = 1` 또는 `1 = 0` 차이를 확인한다. 토큰 원문이 response body/log에 없는지 확인한다.
+
+## 11. 리스크 & 대안 검토
+
+- `V$SQL`/`DBMS_XPLAN` 조회는 connection pool의 다른 세션·cursor·권한 문제 때문에 결과가 불안정하다. 정책 함수 반환값을 같은 context에서 표시하는 방식을 선택한다.
+- Java에서 permission table을 다시 계산하는 방식은 그룹 상속·DENY 우선순위·custom policy와 어긋날 수 있다. DB의 정책 함수 자체를 trace source로 사용한다.
+- Handler 응답 계약을 강제로 변경하면 기존 Handler가 실패할 수 있다. optional response fields와 non-failing fallback을 사용한다.
+- fallback에서 context cleanup을 놓치면 pool connection 간 사용자 혼선이 발생한다. `finally` cleanup을 고정하고 cleanup 오류는 결과를 덮지 않는다.
+
+## 12. 미해결 질문 (Open Questions)
+
+- custom VPD Filter까지 정책 함수별로 자동 trace할지는 별도 이슈로 남긴다.
+- 운영 DB에서 `ADMIN` 이외의 VPD 소유자 구조를 지원하려면 함수 owner 설정을 명시적으로 추가해야 한다.
diff --git a/docs/design/567-ords-vpd-sql-trace/fn-addLocalSqlTrace.md b/docs/design/567-ords-vpd-sql-trace/fn-addLocalSqlTrace.md
new file mode 100644
index 0000000..70f229e
--- /dev/null
+++ b/docs/design/567-ords-vpd-sql-trace/fn-addLocalSqlTrace.md
@@ -0,0 +1,73 @@
+# 함수 설계서: `addLocalSqlTrace` (#567)
+
+> **부모 설계서**: ./README.md · **상태**: Approved
+> **작성**: [AI] Architect · **구현**: `src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java:addLocalSqlTrace` · **테스트**: `ProbeResultTest`, `mvn test`
+
+## 1. 시그니처
+
+```java
+private ProbeResult addLocalSqlTrace(ProbeResult result, String bearerToken, ProtectedObject object)
+```
+
+## 2. 책임 (단일 책임, 1줄)
+
+ORDS 응답에 trace가 없을 때 DB predicate와 보호 객체 컬럼으로 확인용 effective SQL을 만든다.
+
+## 3. 입력
+
+| 파라미터 | 타입 | 제약/검증 | 설명 |
+|----------|------|-----------|------|
+| `result` | `ProbeResult` | non-null | 이미 파싱된 성공/0행 결과 |
+| `bearerToken` | `String` | 원문은 메모리에서만 사용 | DB context setter에 전달할 토큰 |
+| `object` | `ProtectedObject` | 활성·식별자 검증 완료 | SQL FROM 대상 |
+
+## 4. 출력
+
+- **반환**: predicate와 effective SQL이 부착된 새 `ProbeResult`; 조회할 수 없으면 원 결과.
+- **부수효과**: predicate 조회는 `findVpdPredicate`를 통한 DB I/O다. 결과 rows/status/evidence는 변경하지 않는다.
+
+## 5. 동작 / 알고리즘
+
+1. bearer/object로 `findVpdPredicate`를 호출한다.
+2. predicate가 없으면 원 결과를 반환한다.
+3. 보호 객체의 등록 컬럼을 `o.COLUMN` 목록으로 만든다.
+4. `SELECT columns FROM owner.object o WHERE (predicate) AND ROWNUM <= ...` 문자열을 만든다.
+5. `ProbeResult.withSqlTrace`로 불변 결과를 반환한다.
+
+## 6. 에러 & 실패 모드
+
+| 조건 | 처리 | 반환/예외 |
+|------|------|-----------|
+| context/predicate DB 오류 | `findVpdPredicate`가 null 반환 | 원 결과 |
+| 컬럼 catalog 오류 | runtime exception을 경계에서 흡수 | 원 결과 |
+| 컬럼 목록 empty | trace 생성 생략 | 원 결과 |
+| vector object | 호출부에서 이 함수 실행을 생략 | 전용 Handler trace만 사용 |
+
+## 7. 엣지케이스
+
+- 0행 결과도 호출 가능해야 한다.
+- predicate의 `SYS_CONTEXT` 표현은 실제 값으로 치환하지 않는다.
+- 컬럼명은 보호 객체 catalog에서 가져오며 임의 입력을 SQL에 추가하지 않는다.
+- token은 반환 문자열·로그에 포함하지 않는다.
+
+## 8. 복잡도 / 성능
+
+- DB predicate 조회 1회와 컬럼 cache 조회 1회. 컬럼 수를 `n`이라 하면 SQL 문자열 조합은 `O(n)`이다.
+- ORDS trace가 없는 경우에만 추가 DB 연결 작업을 수행한다.
+
+## 9. 의존성
+
+- `findVpdPredicate`, `ProtectedObjectService.findColumns`, `ProbeResult.withSqlTrace`.
+- `ADMIN.CB_AGENT_DOC_VPD_FILTER`와 보호 객체 catalog.
+
+## 10. 테스트 케이스
+
+- [x] trace 부착 후 rows/status/rowCount 보존.
+- [ ] predicate 조회 실패 시 원 결과 유지.
+- [ ] empty columns 시 trace 생략.
+- [ ] vector object는 호출부에서 생략.
+
+## 11. 추적성
+
+- 인수조건: #567의 “유효 토큰 조회 결과에 predicate/effective SQL 표시”.
+- 관련 ADR: 없음.
diff --git a/docs/design/567-ords-vpd-sql-trace/fn-findVpdPredicate.md b/docs/design/567-ords-vpd-sql-trace/fn-findVpdPredicate.md
new file mode 100644
index 0000000..a175bfe
--- /dev/null
+++ b/docs/design/567-ords-vpd-sql-trace/fn-findVpdPredicate.md
@@ -0,0 +1,73 @@
+# 함수 설계서: `findVpdPredicate` (#567)
+
+> **부모 설계서**: ./README.md · **상태**: Approved
+> **작성**: [AI] Architect · **구현**: `src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java:findVpdPredicate` · **테스트**: `mvn test`, 운영 smoke
+
+## 1. 시그니처
+
+```java
+private String findVpdPredicate(String bearerToken, ProtectedObject object)
+```
+
+## 2. 책임 (단일 책임, 1줄)
+
+하나의 pooled DB connection에서 Bearer 기반 context를 설정하고 동일 VPD 함수의 predicate를 조회한 뒤 context를 제거한다.
+
+## 3. 입력
+
+| 파라미터 | 타입 | 제약/검증 | 설명 |
+|----------|------|-----------|------|
+| `bearerToken` | `String` | 상위 token 검증 통과 | `set_user_by_bearer` 입력 |
+| `object` | `ProtectedObject` | 활성 object | predicate의 schema/object 입력 |
+
+## 4. 출력
+
+- **반환**: `ADMIN.CB_AGENT_DOC_VPD_FILTER(owner, object)`의 predicate 문자열 또는 조회 불가 시 null.
+- **부수효과**: 같은 connection의 secure application context를 잠시 설정하고 반드시 clear한다. token 원문은 DB 함수 호출 이외의 출력으로 내보내지 않는다.
+
+## 5. 동작 / 알고리즘
+
+1. `JdbcTemplate.execute(ConnectionCallback)`으로 connection을 확보한다.
+2. `BEGIN admin.cb_agent_ctx_pkg.set_user_by_bearer(?); END;`를 실행한다.
+3. `SELECT admin.cb_agent_doc_vpd_filter(?, ?) FROM dual`을 실행한다.
+4. `finally`에서 `BEGIN admin.cb_agent_ctx_pkg.clear_user; END;`를 실행한다.
+5. Spring JDBC/DB runtime 오류는 trace optional 경로에서 null로 변환한다.
+
+## 6. 에러 & 실패 모드
+
+| 조건 | 처리 | 반환/예외 |
+|------|------|-----------|
+| DB 연결/권한 오류 | fallback 경계에서 처리 | null |
+| Bearer 매핑 실패 | DB package 오류 | null, 원 ORDS 결과 유지 |
+| predicate 함수 오류 | DB query 오류 | null, 원 ORDS 결과 유지 |
+| cleanup 오류 | 별도 오류로 덮지 않음 | predicate 결과 또는 null |
+
+## 7. 엣지케이스
+
+- context setter가 실패해도 `finally` cleanup을 시도한다.
+- connection pool에 context가 남지 않아야 한다.
+- ORDS와 백오피스가 다른 DB/계정인 구성에서는 null이 정상 fallback 결과일 수 있다.
+- `ADMIN` 이외 함수 owner는 이번 설계에서 지원하지 않는다.
+
+## 8. 복잡도 / 성능
+
+- trace가 없는 성공/0행 조회당 DB connection 1회, PL/SQL 1회, predicate SELECT 1회.
+- 결과 행 수나 token 수에 비례하지 않는 `O(1)` DB 호출이며, Handler trace가 있으면 호출하지 않는다.
+
+## 9. 의존성
+
+- Spring `JdbcTemplate`/`ConnectionCallback`.
+- `ADMIN.CB_AGENT_CTX_PKG`, `ADMIN.CB_AGENT_DOC_VPD_FILTER`.
+- connection pool cleanup semantics.
+
+## 10. 테스트 케이스
+
+- [ ] context setter와 predicate query가 같은 connection에서 실행됨.
+- [ ] query 성공 후 clear가 실행됨.
+- [ ] setter/query 실패 후 clear가 시도됨.
+- [ ] DB privilege 미설정 시 원 ORDS 결과가 유지됨.
+
+## 11. 추적성
+
+- 인수조건: #567의 “기존 Handler도 trace fallback 제공” 및 “trace 실패가 권한 결과를 실패로 만들지 않음”.
+- 관련 ADR: 없음.
diff --git a/docs/reference/ords-vpd-sql-trace.md b/docs/reference/ords-vpd-sql-trace.md
new file mode 100644
index 0000000..6966cb0
--- /dev/null
+++ b/docs/reference/ords-vpd-sql-trace.md
@@ -0,0 +1,17 @@
+# ORDS/VPD SQL trace
+
+권한 결과 확인(`/probe`)은 ORDS 응답의 optional `vpd_predicate`, `effective_sql` 필드를 “토큰 적용 후 SQL” 영역에 표시한다.
+
+```json
+{
+  "vpd_predicate": "(DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))",
+  "effective_sql": "SELECT ... WHERE (...) AND ROWNUM <= ...",
+  "items": []
+}
+```
+
+기존 Handler가 trace 필드를 반환하지 않으면 백오피스가 같은 DB 연결에서 `set_user_by_bearer` → `CB_AGENT_DOC_VPD_FILTER` → `clear_user` 순서로 기본 VPD predicate를 조회한다. 이 보조 조회가 실패해도 권한 결과 자체는 실패 처리하지 않는다.
+
+기존 설치에 trace 권한을 추가하려면 ADMIN으로 `sql/adb/33_agent_ords_sql_trace_grant.sql`을 실행한 뒤 CB_ORDS로 `22_agent_ords_security_ords_handler_setup.sql`을 재실행하거나 `/ords-handlers`에서 trace가 포함된 Handler source를 저장한다.
+
+표시되는 SQL은 Oracle optimizer 실행계획이 아니라, 해당 토큰 컨텍스트에서 VPD 정책 함수가 반환한 행 predicate를 기본 Handler SELECT에 결합한 확인용 SQL이다. Bearer 원문은 trace나 응답에 포함하지 않는다.
diff --git a/sql/adb/17_agent_ords_security_local_vpd_setup.sql b/sql/adb/17_agent_ords_security_local_vpd_setup.sql
index f0b0d56..c4369ff 100644
--- a/sql/adb/17_agent_ords_security_local_vpd_setup.sql
+++ b/sql/adb/17_agent_ords_security_local_vpd_setup.sql
@@ -477,6 +477,7 @@ GRANT CREATE SESSION TO cb_ords;
 GRANT CREATE PROCEDURE TO cb_ords;
 GRANT SELECT ON cb_v_search_documents TO cb_ords;
 GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
+GRANT EXECUTE ON cb_agent_doc_vpd_filter TO cb_ords;
 GRANT EXECUTE ON cb_agent_can_read_column TO cb_ords;
 
 PROMPT === Local VPD setup complete ===
diff --git a/sql/adb/22_agent_ords_security_ords_handler_setup.sql b/sql/adb/22_agent_ords_security_ords_handler_setup.sql
index 47d7e9a..8e93086 100644
--- a/sql/adb/22_agent_ords_security_ords_handler_setup.sql
+++ b/sql/adb/22_agent_ords_security_ords_handler_setup.sql
@@ -90,9 +90,24 @@ BEGIN
     p_source         => q'!
 DECLARE
   v_rows SYS_REFCURSOR;
+  v_vpd_predicate VARCHAR2(32767);
+  v_effective_sql VARCHAR2(32767);
 BEGIN
   cb_ords_handler_pkg.set_vpd_context(:auth_header);
 
+  -- The predicate below is the value returned by the same DBMS_RLS policy
+  -- function that protects ADMIN.CB_V_SEARCH_DOCUMENTS.
+  BEGIN
+    v_vpd_predicate := admin.cb_agent_doc_vpd_filter('ADMIN', 'CB_V_SEARCH_DOCUMENTS');
+    v_effective_sql := 'SELECT d.doc_id, d.title, d.owner_emp_no, d.dept_code, d.contents '
+      || 'FROM admin.cb_v_search_documents d WHERE (' || v_vpd_predicate || ') '
+      || 'AND ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 50), 1), 500)';
+  EXCEPTION
+    WHEN OTHERS THEN
+      v_vpd_predicate := NULL;
+      v_effective_sql := NULL;
+  END;
+
   OPEN v_rows FOR
     SELECT d.doc_id,
            d.title,
@@ -108,6 +123,10 @@ BEGIN
   OWA_UTIL.HTTP_HEADER_CLOSE;
 
   APEX_JSON.OPEN_OBJECT;
+  IF v_effective_sql IS NOT NULL THEN
+    APEX_JSON.WRITE('vpd_predicate', v_vpd_predicate);
+    APEX_JSON.WRITE('effective_sql', v_effective_sql);
+  END IF;
   APEX_JSON.WRITE('items', v_rows);
   APEX_JSON.CLOSE_OBJECT;
 
diff --git a/sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql b/sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
index 90360aa..6e7bc35 100644
--- a/sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
+++ b/sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
@@ -200,4 +200,9 @@ END;
 
 SHOW ERRORS FUNCTION cb_agent_doc_vpd_filter
 
+-- ORDS SQL tracing calls the same policy function after set_vpd_context so
+-- the response can expose the token-specific VPD predicate.  This is
+-- diagnostic metadata only; the SELECT remains protected by DBMS_RLS.
+GRANT EXECUTE ON cb_agent_doc_vpd_filter TO cb_ords;
+
 PROMPT === Dynamic whitelist VPD filter ready ===
diff --git a/sql/adb/29_agent_ords_vector_search_ords.sql b/sql/adb/29_agent_ords_vector_search_ords.sql
index 9f1b409..2d6ece0 100644
--- a/sql/adb/29_agent_ords_vector_search_ords.sql
+++ b/sql/adb/29_agent_ords_vector_search_ords.sql
@@ -50,9 +50,24 @@ DECLARE
   v_rows           SYS_REFCURSOR;
   v_body_text      CLOB;
   v_embedding_text VARCHAR2(32767);
+  v_vpd_predicate  VARCHAR2(32767);
+  v_effective_sql  VARCHAR2(32767);
 BEGIN
   cb_ords_handler_pkg.set_vpd_context(:auth_header);
 
+  BEGIN
+    v_vpd_predicate := admin.cb_agent_doc_vpd_filter('ADMIN', 'CB_VECTOR_SEARCH_DOCUMENTS');
+    v_effective_sql := 'SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score '
+      || 'FROM (SELECT ... FROM admin.cb_vector_search_documents d '
+      || 'WHERE d.embedding IS NOT NULL /* VPD: ' || v_vpd_predicate || ' */ '
+      || 'ORDER BY score) ranked_chunks '
+      || 'WHERE ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 10), 1), 100)';
+  EXCEPTION
+    WHEN OTHERS THEN
+      v_vpd_predicate := NULL;
+      v_effective_sql := NULL;
+  END;
+
   -- ORDS exposes :body_text as a stream bind. Read it exactly once; referring
   -- to the stream bind twice causes ORA-17270 (Duplicate stream parameter).
   v_body_text := :body_text;
@@ -108,6 +123,10 @@ BEGIN
   OWA_UTIL.HTTP_HEADER_CLOSE;
 
   APEX_JSON.OPEN_OBJECT;
+  IF v_effective_sql IS NOT NULL THEN
+    APEX_JSON.WRITE('vpd_predicate', v_vpd_predicate);
+    APEX_JSON.WRITE('effective_sql', v_effective_sql);
+  END IF;
   APEX_JSON.WRITE('items', v_rows);
   APEX_JSON.CLOSE_OBJECT;
 
diff --git a/sql/adb/33_agent_ords_sql_trace_grant.sql b/sql/adb/33_agent_ords_sql_trace_grant.sql
new file mode 100644
index 0000000..da10c2a
--- /dev/null
+++ b/sql/adb/33_agent_ords_sql_trace_grant.sql
@@ -0,0 +1,21 @@
+-- ============================================================
+-- 33_agent_ords_sql_trace_grant.sql
+-- Enable the optional VPD predicate trace for an existing ORDS install.
+--
+-- Run as ADMIN after 26_agent_ords_security_dynamic_vpd_filter.sql.
+-- Then run 22_agent_ords_security_ords_handler_setup.sql as CB_ORDS, or
+-- save the trace-enabled Handler source from the backoffice UI.
+--
+-- The grant exposes only the existing predicate function to the ORDS
+-- runtime. It does not weaken DBMS_RLS enforcement or expose bearer values.
+-- ============================================================
+WHENEVER SQLERROR EXIT SQL.SQLCODE
+SET ECHO ON
+SET FEEDBACK ON
+
+PROMPT === Enabling ORDS VPD predicate trace ===
+GRANT EXECUTE ON cb_agent_doc_vpd_filter TO cb_ords;
+
+PROMPT === ORDS VPD predicate trace grant ready ===
+PROMPT Next: run 22_agent_ords_security_ords_handler_setup.sql as CB_ORDS
+EXIT;
diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeResult.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeResult.java
index c0ddfd7..735190f 100644
--- a/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeResult.java
+++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeResult.java
@@ -14,9 +14,41 @@ public record ProbeResult(
     String requestHeaders,
     String requestPayload,
     String responseHeaders,
-    String responseBody
+    String responseBody,
+    String vpdPredicate,
+    String effectiveSql
 ) {
 
+  public ProbeResult(
+      ProbeStatus status,
+      List columns,
+      List> rows,
+      int rowCount,
+      List maskedColumns,
+      String errorCode,
+      String errorMessage,
+      String requestHeaders,
+      String requestPayload,
+      String responseHeaders,
+      String responseBody
+  ) {
+    this(
+        status,
+        columns,
+        rows,
+        rowCount,
+        maskedColumns,
+        errorCode,
+        errorMessage,
+        requestHeaders,
+        requestPayload,
+        responseHeaders,
+        responseBody,
+        null,
+        null
+    );
+  }
+
   public static ProbeResult blocked(ProbeStatus status, String errorCode, String errorMessage) {
     return blocked(status, errorCode, errorMessage, null, null, null, null);
   }
@@ -49,6 +81,28 @@ public record ProbeResult(
     return status == ProbeStatus.SUCCESS || status == ProbeStatus.VPD_DENY_EMPTY_RESULT;
   }
 
+  public boolean hasSqlTrace() {
+    return effectiveSql != null && !effectiveSql.isBlank();
+  }
+
+  public ProbeResult withSqlTrace(String predicate, String sql) {
+    return new ProbeResult(
+        status,
+        columns,
+        rows,
+        rowCount,
+        maskedColumns,
+        errorCode,
+        errorMessage,
+        requestHeaders,
+        requestPayload,
+        responseHeaders,
+        responseBody,
+        predicate,
+        sql
+    );
+  }
+
   public String title() {
     return switch (status) {
       case SUCCESS -> "권한에 따라 데이터를 볼 수 있습니다.";
diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataService.java
index 883a2e5..23f52b6 100644
--- a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataService.java
+++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataService.java
@@ -249,12 +249,30 @@ public class OrdsMetadataService {
         .map(column -> "o." + column)
         .reduce((left, right) -> left + ",\n       " + right)
         .orElseThrow();
+    String traceSelectColumns = columns.stream()
+        .map(column -> "o." + column)
+        .reduce((left, right) -> left + ", " + right)
+        .orElseThrow();
     return """
         DECLARE
           v_rows SYS_REFCURSOR;
+          v_vpd_predicate VARCHAR2(32767);
+          v_effective_sql VARCHAR2(32767);
         BEGIN
           cb_ords_handler_pkg.set_vpd_context(:auth_header);
 
+          -- This is the same predicate function invoked by DBMS_RLS for the
+          -- SELECT below. It is returned only as diagnostic metadata.
+          BEGIN
+            v_vpd_predicate := admin.cb_agent_doc_vpd_filter('%s', '%s');
+            v_effective_sql := 'SELECT %s FROM %s.%s o WHERE (' || v_vpd_predicate
+                || ') AND ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 50), 1), 500)';
+          EXCEPTION
+            WHEN OTHERS THEN
+              v_vpd_predicate := NULL;
+              v_effective_sql := NULL;
+          END;
+
           OPEN v_rows FOR
             SELECT %s
             FROM   %s.%s o
@@ -266,6 +284,10 @@ public class OrdsMetadataService {
           OWA_UTIL.HTTP_HEADER_CLOSE;
 
           APEX_JSON.OPEN_OBJECT;
+          IF v_effective_sql IS NOT NULL THEN
+            APEX_JSON.WRITE('vpd_predicate', v_vpd_predicate);
+            APEX_JSON.WRITE('effective_sql', v_effective_sql);
+          END IF;
           APEX_JSON.WRITE('items', v_rows);
           APEX_JSON.CLOSE_OBJECT;
 
@@ -279,9 +301,17 @@ public class OrdsMetadataService {
             OWA_UTIL.HTTP_HEADER_CLOSE;
             APEX_JSON.OPEN_OBJECT;
             APEX_JSON.WRITE('error', SQLERRM);
-            APEX_JSON.CLOSE_OBJECT;
+          APEX_JSON.CLOSE_OBJECT;
         END;
-        """.formatted(selectColumns, object.owner(), object.objectName());
+        """.formatted(
+            object.owner(),
+            object.objectName(),
+            traceSelectColumns,
+            object.owner(),
+            object.objectName(),
+            selectColumns,
+            object.owner(),
+            object.objectName());
   }
 
   private void rejectGenericVectorHandler(ProtectedObject object) {
diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java
index bb920b3..46f7914 100644
--- a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java
+++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java
@@ -11,6 +11,9 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.net.URI;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
 import java.time.Clock;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
@@ -20,11 +23,15 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import org.springframework.jdbc.core.ConnectionCallback;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.HttpStatusCodeException;
 import org.springframework.web.client.ResourceAccessException;
@@ -36,6 +43,7 @@ public class OrdsProbeService {
 
   private static final TypeReference> MAP_TYPE = new TypeReference<>() {
   };
+  private static final Logger log = LoggerFactory.getLogger(OrdsProbeService.class);
 
   private final BearerTokenService tokenService;
   private final ProtectedObjectService protectedObjectService;
@@ -44,6 +52,7 @@ public class OrdsProbeService {
   private final RestTemplate ordsRestTemplate;
   private final ObjectMapper objectMapper;
   private final SettingService settingService;
+  private final JdbcTemplate jdbcTemplate;
   private final Clock clock;
 
   public OrdsProbeService(
@@ -54,6 +63,7 @@ public class OrdsProbeService {
       RestTemplate ordsRestTemplate,
       ObjectMapper objectMapper,
       SettingService settingService,
+      JdbcTemplate jdbcTemplate,
       Clock clock
   ) {
     this.tokenService = tokenService;
@@ -63,6 +73,7 @@ public class OrdsProbeService {
     this.ordsRestTemplate = ordsRestTemplate;
     this.objectMapper = objectMapper;
     this.settingService = settingService;
+    this.jdbcTemplate = jdbcTemplate;
     this.clock = clock;
   }
 
@@ -131,6 +142,9 @@ public class OrdsProbeService {
           prettyHeaders(response.getHeaders()),
           prettyJson(response.getBody())
       );
+      if (!result.hasSqlTrace() && !isVectorSearchObject(object)) {
+        result = addLocalSqlTrace(result, command.bearerToken(), object);
+      }
       return auditAndReturn(command, result);
     } catch (HttpStatusCodeException e) {
       ProbeStatus status = errorClassifier.classify(e.getStatusCode(), e.getResponseBodyAsString());
@@ -205,10 +219,93 @@ public class OrdsProbeService {
         requestHeaders,
         requestPayload,
         responseHeaders,
-        responseBody
+        responseBody,
+        traceValue(root, "vpd_predicate"),
+        traceValue(root, "effective_sql")
     );
   }
 
+  private String traceValue(JsonNode root, String fieldName) {
+    JsonNode value = root == null ? null : root.get(fieldName);
+    if (value == null || value.isNull() || !value.isValueNode()) {
+      return null;
+    }
+    String text = value.asText();
+    return text == null || text.isBlank() ? null : text;
+  }
+
+  private ProbeResult addLocalSqlTrace(ProbeResult result, String bearerToken, ProtectedObject object) {
+    String predicate = findVpdPredicate(bearerToken, object);
+    if (predicate == null || predicate.isBlank()) {
+      return result;
+    }
+    List columns;
+    try {
+      columns = protectedObjectService.findColumns(object.objectId()).stream()
+          .map(column -> "o." + column.columnName())
+          .toList();
+    } catch (RuntimeException ignored) {
+      return result;
+    }
+    if (columns.isEmpty()) {
+      return result;
+    }
+    String effectiveSql = "SELECT " + String.join(", ", columns)
+        + " FROM " + object.owner() + "." + object.objectName() + " o"
+        + " WHERE (" + predicate + ")"
+        + " AND ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 50), 1), 500)";
+    return result.withSqlTrace(predicate, effectiveSql);
+  }
+
+  private String findVpdPredicate(String bearerToken, ProtectedObject object) {
+    try {
+      return jdbcTemplate.execute((ConnectionCallback) connection -> {
+        try {
+          executeContextSetter(connection, bearerToken);
+          try (PreparedStatement statement = connection.prepareStatement(
+              "SELECT admin.cb_agent_doc_vpd_filter(?, ?) FROM dual")) {
+            statement.setString(1, object.owner());
+            statement.setString(2, object.objectName());
+            try (ResultSet result = statement.executeQuery()) {
+              return result.next() ? result.getString(1) : null;
+            }
+          }
+        } finally {
+          clearContext(connection);
+        }
+      });
+    } catch (RuntimeException exception) {
+      // The backoffice may use a different DB account or a database without
+      // the optional trace privilege. The ORDS response remains authoritative
+      // when the Handler itself returned trace fields.
+      log.debug("VPD SQL trace unavailable for {}.{}: {}",
+          object.owner(), object.objectName(), exception.getMessage());
+      return null;
+    }
+  }
+
+  private void executeContextSetter(Connection connection, String bearerToken) throws java.sql.SQLException {
+    try (PreparedStatement statement = connection.prepareStatement(
+        "BEGIN admin.cb_agent_ctx_pkg.set_user_by_bearer(?); END;")) {
+      statement.setString(1, bearerToken);
+      statement.execute();
+    }
+  }
+
+  private void clearContext(Connection connection) {
+    try (PreparedStatement statement = connection.prepareStatement(
+        "BEGIN admin.cb_agent_ctx_pkg.clear_user; END;")) {
+      statement.execute();
+    } catch (java.sql.SQLException exception) {
+      // Do not replace a successful ORDS result with a diagnostic cleanup error.
+      log.debug("VPD context cleanup failed: {}", exception.getMessage());
+    }
+  }
+
+  private boolean isVectorSearchObject(ProtectedObject object) {
+    return "CB_VECTOR_SEARCH_DOCUMENTS".equalsIgnoreCase(object.objectName());
+  }
+
   private List findMaskedColumns(long objectId, List> rows) {
     if (rows.isEmpty()) {
       return List.of();
diff --git a/src/main/resources/templates/fragments/probe-result.html b/src/main/resources/templates/fragments/probe-result.html
index 2d3df59..a5fe9bb 100644
--- a/src/main/resources/templates/fragments/probe-result.html
+++ b/src/main/resources/templates/fragments/probe-result.html
@@ -70,6 +70,33 @@
     
   
 
+  
+
+
+

토큰 적용 후 SQL

+

ORDS Handler가 반환한 VPD predicate 또는 같은 토큰 컨텍스트를 재현해 조회한 predicate를 기본 조회문에 합친 형태입니다.

+
+ DBMS_RLS predicate +
+
+
+

VPD가 추가한 WHERE 조건

+
(DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))
+
+
+

권한 적용 SQL

+
SELECT ... WHERE (...) AND ROWNUM <= ...
+
+
+

+ Oracle 내부 optimizer의 실행계획이나 bind 값 치환 결과가 아니라, 이 토큰 컨텍스트에서 VPD 정책 함수가 실제로 반환한 행 조건을 표시합니다. 컬럼 마스킹은 별도 Redaction 정책입니다. +

+
+ +
+ 이 Handler는 SQL trace 정보를 반환하지 않았고 백오피스 DB에서도 기본 VPD predicate를 조회하지 못했습니다. ORDS 핸들러에서 trace가 포함된 소스를 적용하고, CB_AGENT_DOC_VPD_FILTER 실행 권한을 확인하세요. +
+

다음에 할 일

다음 행동

diff --git a/src/main/resources/templates/ords-handlers.html b/src/main/resources/templates/ords-handlers.html index 713142f..c639ea9 100644 --- a/src/main/resources/templates/ords-handlers.html +++ b/src/main/resources/templates/ords-handlers.html @@ -9,10 +9,14 @@

토큰을 DB 권한 컨텍스트로 바꾸는 Handler를 확인·수정합니다.

Handler 처리 흐름 설명 보기 -

Bearer Token을 DB 컨텍스트로 변환한 뒤 VPD가 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다.

+

Bearer Token을 DB 컨텍스트로 변환한 뒤 VPD가 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다. 실행 결과의 “토큰 적용 후 SQL”은 권한 결과 확인 메뉴에서 확인할 수 있습니다.

+
+ 권한별로 VPD가 붙인 실제 행 조건을 확인하려면 권한 결과 확인에서 이 Handler의 보호 객체를 실행하세요. SQL trace를 반환하도록 갱신된 Handler는 같은 ORDS 세션의 CB_AGENT_DOC_VPD_FILTER predicate를 함께 표시합니다. +
+
diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeResultTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeResultTest.java index 6800333..bff5b59 100644 --- a/src/test/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeResultTest.java +++ b/src/test/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeResultTest.java @@ -97,4 +97,29 @@ class ProbeResultTest { assertThat(result.plainSummary()).contains("토큰과 사용자 권한은 확인"); assertThat(result.nextAction()).contains("토큰이나 권한을 바꾸지 말고").contains("자동 Filter"); } + + @Test + void exposesVpdSqlTraceWithoutChangingTheRows() { + ProbeResult result = new ProbeResult( + ProbeStatus.SUCCESS, + List.of("DEPT_CODE"), + List.of(Map.of("DEPT_CODE", "HR")), + 1, + List.of(), + null, + null, + null, + null, + null, + null + ).withSqlTrace( + "(DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))", + "SELECT * FROM ADMIN.DOCUMENTS WHERE (DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))" + ); + + assertThat(result.hasSqlTrace()).isTrue(); + assertThat(result.vpdPredicate()).contains("DEPT_CODE"); + assertThat(result.effectiveSql()).contains("ADMIN.DOCUMENTS"); + assertThat(result.rowCount()).isEqualTo(1); + } } diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataServiceTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataServiceTest.java new file mode 100644 index 0000000..4327593 --- /dev/null +++ b/src/test/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataServiceTest.java @@ -0,0 +1,54 @@ +package com.cloudhandson.vpdbackoffice.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; +import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject; +import com.cloudhandson.vpdbackoffice.mapper.OrdsMetadataMapper; +import java.util.List; +import javax.sql.DataSource; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; + +class OrdsMetadataServiceTest { + + @Test + void generatedObjectHandlerIncludesTokenSpecificVpdSqlTrace() { + OrdsMetadataMapper mapper = mock(OrdsMetadataMapper.class); + ProtectedObjectService protectedObjectService = mock(ProtectedObjectService.class); + DataSource dataSource = mock(DataSource.class); + ProtectedObject object = new ProtectedObject( + 7L, + "ADMIN", + "CB_V_SEARCH_DOCUMENTS", + "cb-ords/cb-object-query/admin/cb_v_search_documents", + "Y" + ); + when(protectedObjectService.assertEnabled(7L)).thenReturn(object); + when(protectedObjectService.findColumns(7L)).thenReturn(List.of( + new ProtectedColumn(1L, 7L, "DOC_ID", "N", null, "PUBLIC", "NONE"), + new ProtectedColumn(2L, 7L, "DEPT_CODE", "N", null, "INTERNAL", "NONE") + )); + + OrdsMetadataService service = new OrdsMetadataService( + mapper, + new JdbcTemplate(dataSource), + protectedObjectService, + dataSource, + "", + "", + "" + ); + + String source = service.objectQueryHandlerSource(7L); + + assertThat(source) + .contains("cb_ords_handler_pkg.set_vpd_context(:auth_header)") + .contains("admin.cb_agent_doc_vpd_filter('ADMIN', 'CB_V_SEARCH_DOCUMENTS')") + .contains("v_vpd_predicate") + .contains("APEX_JSON.WRITE('effective_sql', v_effective_sql)") + .contains("SELECT o.DOC_ID, o.DEPT_CODE FROM ADMIN.CB_V_SEARCH_DOCUMENTS o"); + } +} diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java index d888fa2..ae1564c 100644 --- a/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java +++ b/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java @@ -31,6 +31,8 @@ class GuidedFlowTemplateTest { .doesNotContain("name=\"tokenKeyId\""); assertThat(result) .contains("적용된 사용자와 권한") + .contains("토큰 적용 후 SQL") + .contains("vpd_predicate") .contains("다음에 할 일") .contains("