[Developer] #567 expose VPD effective SQL trace
This commit is contained in:
137
docs/design/567-ords-vpd-sql-trace/README.md
Normal file
137
docs/design/567-ords-vpd-sql-trace/README.md
Normal file
@@ -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<String>)` | 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)`)와 화면 `<pre>`로 처리한다.
|
||||
|
||||
## 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 설정을 명시적으로 추가해야 한다.
|
||||
73
docs/design/567-ords-vpd-sql-trace/fn-addLocalSqlTrace.md
Normal file
73
docs/design/567-ords-vpd-sql-trace/fn-addLocalSqlTrace.md
Normal file
@@ -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: 없음.
|
||||
73
docs/design/567-ords-vpd-sql-trace/fn-findVpdPredicate.md
Normal file
73
docs/design/567-ords-vpd-sql-trace/fn-findVpdPredicate.md
Normal file
@@ -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: 없음.
|
||||
17
docs/reference/ords-vpd-sql-trace.md
Normal file
17
docs/reference/ords-vpd-sql-trace.md
Normal file
@@ -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나 응답에 포함하지 않는다.
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
21
sql/adb/33_agent_ords_sql_trace_grant.sql
Normal file
21
sql/adb/33_agent_ords_sql_trace_grant.sql
Normal file
@@ -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;
|
||||
@@ -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<String> columns,
|
||||
List<Map<String, Object>> rows,
|
||||
int rowCount,
|
||||
List<String> 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 -> "권한에 따라 데이터를 볼 수 있습니다.";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<String, Object>> 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<String> 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<String>) 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<String> findMaskedColumns(long objectId, List<Map<String, Object>> rows) {
|
||||
if (rows.isEmpty()) {
|
||||
return List.of();
|
||||
|
||||
@@ -70,6 +70,33 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="result-section sql-trace-section" th:if="${result.hasSqlTrace()}">
|
||||
<div class="section-heading compact-heading">
|
||||
<div>
|
||||
<h3>토큰 적용 후 SQL</h3>
|
||||
<p class="section-subtitle">ORDS Handler가 반환한 VPD predicate 또는 같은 토큰 컨텍스트를 재현해 조회한 predicate를 기본 조회문에 합친 형태입니다.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-light">DBMS_RLS predicate</span>
|
||||
</div>
|
||||
<div class="probe-exchange-grid mt-3">
|
||||
<section class="probe-exchange" data-sql-trace-field="vpd_predicate">
|
||||
<h3>VPD가 추가한 WHERE 조건</h3>
|
||||
<pre th:text="${result.vpdPredicate()}">(DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))</pre>
|
||||
</section>
|
||||
<section class="probe-exchange" data-sql-trace-field="effective_sql">
|
||||
<h3>권한 적용 SQL</h3>
|
||||
<pre th:text="${result.effectiveSql()}">SELECT ... WHERE (...) AND ROWNUM <= ...</pre>
|
||||
</section>
|
||||
</div>
|
||||
<p class="form-hint mt-2 mb-0">
|
||||
Oracle 내부 optimizer의 실행계획이나 bind 값 치환 결과가 아니라, 이 토큰 컨텍스트에서 VPD 정책 함수가 실제로 반환한 행 조건을 표시합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="alert alert-light mb-0" th:if="${result.successLike() and !result.hasSqlTrace()}">
|
||||
이 Handler는 SQL trace 정보를 반환하지 않았고 백오피스 DB에서도 기본 VPD predicate를 조회하지 못했습니다. <a href="/ords-handlers">ORDS 핸들러</a>에서 trace가 포함된 소스를 적용하고, <code>CB_AGENT_DOC_VPD_FILTER</code> 실행 권한을 확인하세요.
|
||||
</div>
|
||||
|
||||
<section class="next-action-card">
|
||||
<h3>다음에 할 일</h3>
|
||||
<p th:text="${result.nextAction()}">다음 행동</p>
|
||||
|
||||
@@ -9,10 +9,14 @@
|
||||
<p class="context-summary">토큰을 DB 권한 컨텍스트로 바꾸는 Handler를 확인·수정합니다.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>Handler 처리 흐름 설명 보기</summary>
|
||||
<p>Bearer Token을 DB 컨텍스트로 변환한 뒤 VPD가 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다.</p>
|
||||
<p>Bearer Token을 DB 컨텍스트로 변환한 뒤 VPD가 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다. 실행 결과의 “토큰 적용 후 SQL”은 권한 결과 확인 메뉴에서 확인할 수 있습니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
권한별로 VPD가 붙인 실제 행 조건을 확인하려면 <a href="/probe">권한 결과 확인</a>에서 이 Handler의 보호 객체를 실행하세요. SQL trace를 반환하도록 갱신된 Handler는 같은 ORDS 세션의 <code>CB_AGENT_DOC_VPD_FILTER</code> predicate를 함께 표시합니다.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ class GuidedFlowTemplateTest {
|
||||
.doesNotContain("name=\"tokenKeyId\"");
|
||||
assertThat(result)
|
||||
.contains("적용된 사용자와 권한")
|
||||
.contains("토큰 적용 후 SQL")
|
||||
.contains("vpd_predicate")
|
||||
.contains("다음에 할 일")
|
||||
.contains("<details")
|
||||
.contains("기술 상세");
|
||||
@@ -138,6 +140,18 @@ class GuidedFlowTemplateTest {
|
||||
.doesNotContain("JSON_QUERY(\n :body_text")
|
||||
.doesNotContain("JSON_VALUE(\n :body_text");
|
||||
assertThat(sql.split("v_body_text := :body_text", -1)).hasSize(2);
|
||||
assertThat(sql).contains("vpd_predicate").contains("effective_sql");
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingOrdsSetupCanReturnTheVpdPredicateTrace() throws IOException {
|
||||
String setup = Files.readString(Path.of("sql/adb/22_agent_ords_security_ords_handler_setup.sql"));
|
||||
String grant = Files.readString(Path.of("sql/adb/33_agent_ords_sql_trace_grant.sql"));
|
||||
|
||||
assertThat(setup)
|
||||
.contains("admin.cb_agent_doc_vpd_filter('ADMIN', 'CB_V_SEARCH_DOCUMENTS')")
|
||||
.contains("APEX_JSON.WRITE('effective_sql', v_effective_sql)");
|
||||
assertThat(grant).contains("GRANT EXECUTE ON cb_agent_doc_vpd_filter TO cb_ords");
|
||||
}
|
||||
|
||||
private String template(String relativePath) throws IOException {
|
||||
|
||||
Reference in New Issue
Block a user