From d97dab34bdafa37c475cf8944ff38d561ab60a38 Mon Sep 17 00:00:00 2001 From: devmrko Date: Thu, 25 Jun 2026 21:21:48 +0900 Subject: [PATCH] fix #456: add VPD predicate rule tests --- .../456-vpd-predicate-rule-tests/README.md | 93 +++++++++ .../fn-cb_agent_doc_vpd_filter.md | 83 ++++++++ ...17_agent_ords_security_local_vpd_setup.sql | 24 ++- ...agent_ords_security_dynamic_vpd_filter.sql | 28 ++- ..._ords_security_dynamic_vpd_filter_test.sql | 191 ++++++++++++++++++ 5 files changed, 407 insertions(+), 12 deletions(-) create mode 100644 docs/design/456-vpd-predicate-rule-tests/README.md create mode 100644 docs/design/456-vpd-predicate-rule-tests/fn-cb_agent_doc_vpd_filter.md create mode 100644 sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql diff --git a/docs/design/456-vpd-predicate-rule-tests/README.md b/docs/design/456-vpd-predicate-rule-tests/README.md new file mode 100644 index 0000000..1f5475d --- /dev/null +++ b/docs/design/456-vpd-predicate-rule-tests/README.md @@ -0,0 +1,93 @@ +# 설계서: VPD predicate 생성 함수 구조화 및 단위 검증 보강 (#456) + +> **상태**: Approved +> **작성**: [AI] Architect · **최종수정**: 2026-06-25 +> **추적성** — Redmine: #456 · 관련 ADR: 없음 +> · 구현 파일: `sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql`, `sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql` · 테스트: `sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`, `mvn test` + +## 1. 목적 (Why) + +`cb_agent_doc_vpd_filter`가 DB에 저장된 행규칙을 기반으로 whitelist predicate를 만드는지, rule type별 경계 조건이 fail-closed로 동작하는지 재실행 가능한 테스트로 고정한다. + +## 2. 범위 (Scope) + +- **포함**: VPD predicate 생성 함수의 의도 주석 보강, rule type별 predicate 단위 검증 SQL 추가, 임시 테스트 데이터 생성/정리. +- **제외 (out of scope)**: 백오피스 화면 변경, ORDS handler 자동 생성 변경, MCP/LLM 응답 개선. + +## 3. 인수조건 (Acceptance Criteria) + +- [ ] `ALL`, `MY_DEPT`, `SELF`, `=`, `!=` 규칙이 기대 predicate를 생성한다. +- [ ] 권한이 없거나 `USER_ID` 컨텍스트가 없으면 `1 = 0`을 반환한다. +- [ ] 대상 객체에 없는 `rule_column`은 권한 우회로 이어지지 않고 `1 = 0`으로 닫힌다. +- [ ] 테스트 스크립트는 임시 role/permission/rule을 만들고 종료 시 정리한다. +- [ ] 기존 ORDS 검증 결과와 `mvn test`가 유지된다. + +## 4. 컨텍스트 & 제약 + +- 의존성: Oracle ADB, `CB_AGENT_CTX`, `cb_user_role`, `cb_permission`, `cb_permission_rule`, `all_tab_columns`. +- 제약: VPD policy function은 SQL predicate 문자열을 반환하므로 문자열 escape와 컬럼명 검증이 필수다. +- 가정: `p_object`는 DBMS_RLS가 전달하는 실제 object name이고, `cb_permission.target_name`은 대문자 object name으로 저장된다. + +## 5. 아키텍처 개요 + +- `26_agent_ords_security_dynamic_vpd_filter.sql`: 운영 DB에 적용할 predicate 생성 함수. +- `27_agent_ords_security_dynamic_vpd_filter_test.sql`: SQLcl로 실행하는 단위 검증 스크립트. + +``` +CB_AGENT_CTX.USER_ID + -> CB_USER_ROLE + -> CB_PERMISSION(target_name = p_object, action_name = SELECT) + -> CB_PERMISSION_RULE + -> cb_agent_doc_vpd_filter returns SQL predicate +``` + +I/O와 순수 로직 경계: +- I/O: rule 조회, `all_tab_columns` 컬럼 검증. +- 전략 로직: rule type별 predicate 문자열 생성, 기본 거부 처리. + +## 6. 데이터 모델 + +- 입력: `p_schema VARCHAR2`, `p_object VARCHAR2`, `SYS_CONTEXT('CB_AGENT_CTX','USER_ID')`. +- 저장 규칙: `rule_column`, `rule_type`, `rule_value`. +- 출력: VPD predicate `VARCHAR2`, 예: `1 = 0`, `1 = 1`, `(DEPT_CODE = SYS_CONTEXT(...))`. + +## 7. 함수 명세 (Function Specs) + +| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? | +|------|-----------|----------------|------|------|-----------|-------| +| `cb_agent_doc_vpd_filter` | 현재 사용자와 대상 객체의 저장 규칙을 SQL predicate로 변환 | `FUNCTION(p_schema VARCHAR2, p_object VARCHAR2) RETURN VARCHAR2` | schema, object, app context | predicate string | 컨텍스트/권한 없음 `1 = 0` | **복잡** | +| `assert_predicate_contains` | predicate 결과가 기대 문자열을 포함하는지 검증 | PL/SQL local procedure | label, user, object, expected | 없음 | 실패 시 raise_application_error | 단순 | +| `assert_predicate_equals` | predicate 결과가 기대 문자열과 같은지 검증 | PL/SQL local procedure | label, user, object, expected | 없음 | 실패 시 raise_application_error | 단순 | + +## 8. 흐름 / 알고리즘 + +1. 테스트용 role/permission/rule을 생성한다. +2. 사용자별 `cb_agent_ctx_pkg.set_user`로 context를 설정한다. +3. `cb_agent_doc_vpd_filter('ADMIN', object)`를 직접 호출한다. +4. 반환 predicate가 기대 조건을 포함하거나 일치하는지 검증한다. +5. 잘못된 컬럼 규칙은 `1 = 0`을 반환하는지 확인한다. +6. 테스트 데이터를 정리하고 context를 clear한다. + +## 9. 엣지케이스 & 에러 처리 + +- `USER_ID` 없음: `1 = 0`. +- 권한 없음: `1 = 0`. +- rule_column 없음: `MY_DEPT`는 `DEPT_CODE`, `SELF`는 `OWNER_EMP_NO`를 기본 컬럼으로 사용. +- 존재하지 않는 rule_column: 해당 rule 무시, 남는 rule 없으면 `1 = 0`. +- quote 포함 rule_value: single quote escape가 적용된 literal predicate 생성. + +## 10. 테스트 계획 + +- SQLcl: `@sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql` +- SQLcl: `@sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql` +- Maven: `mvn test` +- ORDS smoke: 기존 HR=3, SELF=1, ALL=6 결과 유지 확인. + +## 11. 리스크 & 대안 검토 + +- 선택: PL/SQL 함수 직접 호출 기반 단위 검증. VPD owner bypass 문제와 무관하게 predicate 자체를 빠르게 검증할 수 있다. +- 대안: ORDS만으로 검증. end-to-end는 좋지만 실패 원인이 handler/token/VPD 중 어디인지 분리하기 어렵다. + +## 12. 미해결 질문 (Open Questions) + +- rule type 확장 시 UI enum과 PL/SQL branch를 어떻게 한 곳에서 관리할지 별도 이슈 #457에서 다룬다. diff --git a/docs/design/456-vpd-predicate-rule-tests/fn-cb_agent_doc_vpd_filter.md b/docs/design/456-vpd-predicate-rule-tests/fn-cb_agent_doc_vpd_filter.md new file mode 100644 index 0000000..9db348a --- /dev/null +++ b/docs/design/456-vpd-predicate-rule-tests/fn-cb_agent_doc_vpd_filter.md @@ -0,0 +1,83 @@ +# 함수 설계서: `cb_agent_doc_vpd_filter` (#456) + +> **부모 설계서**: ./README.md · **상태**: Approved +> **작성**: [AI] Architect · **구현**: `sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql:cb_agent_doc_vpd_filter` · **테스트**: `sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql` + +## 1. 시그니처 + +```sql +FUNCTION cb_agent_doc_vpd_filter( + p_schema IN VARCHAR2, + p_object IN VARCHAR2 +) RETURN VARCHAR2 +``` + +## 2. 책임 + +현재 `CB_AGENT_CTX.USER_ID`와 대상 객체명에 매칭되는 저장 행규칙을 읽어 VPD WHERE predicate 문자열을 반환한다. + +## 3. 입력 + +| 파라미터 | 타입 | 제약/검증 | 설명 | +|----------|------|-----------|------| +| `p_schema` | `VARCHAR2` | NULL이면 거부 | 대상 object owner | +| `p_object` | `VARCHAR2` | NULL이면 거부, `target_name` 비교는 대문자 | 대상 table/view name | +| `CB_AGENT_CTX.USER_ID` | context string | 숫자 변환 실패 시 거부 | 현재 bearer token 사용자 | + +## 4. 출력 + +- **반환**: SQL predicate 문자열. +- **부수효과**: 없음. DB 조회만 수행한다. + +## 5. 동작 / 알고리즘 + +1. `USER_ID`를 숫자로 변환한다. 실패하거나 NULL이면 `1 = 0`. +2. `p_object`를 대문자로 정규화한다. +3. 사용자 역할과 권한, 행규칙을 조회한다. +4. `ALL` 규칙이 있으면 즉시 `1 = 1`. +5. 조건형 규칙은 대상 객체에 존재하는 컬럼만 predicate에 추가한다. +6. predicate가 하나도 없으면 `1 = 0`, 있으면 OR 묶음으로 반환한다. + +## 6. 에러 & 실패 모드 + +| 조건 | 처리 | 반환/예외 | +|------|------|-----------| +| context 없음 | 기본 거부 | `1 = 0` | +| 권한 없음 | 기본 거부 | `1 = 0` | +| 컬럼 검증 실패 | 해당 rule 무시 | 남는 rule 없으면 `1 = 0` | +| rule_value quote 포함 | literal escape | escape된 predicate | + +## 7. 엣지케이스 + +- `MY_DEPT`의 `rule_column`이 NULL이면 `DEPT_CODE`. +- `SELF`의 `rule_column`이 NULL이면 `OWNER_EMP_NO`. +- `=`, `!=`는 `rule_column`이 반드시 필요하다. +- 여러 role/rule은 OR 조건으로 누적한다. + +## 8. 복잡도 / 성능 + +- rule 수를 `n`이라고 할 때 O(n)로 predicate를 만든다. +- VPD policy function이므로 SELECT마다 호출될 수 있어 쿼리는 단순 join과 컬럼 existence check로 제한한다. + +## 9. 의존성 + +- `cb_user_role` +- `cb_permission` +- `cb_permission_rule` +- `all_tab_columns` +- `CB_AGENT_CTX` + +## 10. 테스트 케이스 + +- [ ] 정상: ALL 사용자 -> `1 = 1`. +- [ ] 정상: MY_DEPT 사용자 -> `DEPT_CODE = SYS_CONTEXT(...)`. +- [ ] 정상: SELF 사용자 -> `OWNER_EMP_NO = SYS_CONTEXT(...)`. +- [ ] 정상: `=` 규칙 -> `TO_CHAR(DOC_ID) = '1'`. +- [ ] 정상: `!=` 규칙 -> `TO_CHAR(DEPT_CODE) <> 'HR'`. +- [ ] 실패: context 없음 -> `1 = 0`. +- [ ] 실패: 존재하지 않는 컬럼 -> `1 = 0`. + +## 11. 추적성 + +- 인수조건: #456 전체. +- 관련 ADR: 없음. 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 710f372..5179d58 100644 --- a/sql/adb/17_agent_ords_security_local_vpd_setup.sql +++ b/sql/adb/17_agent_ords_security_local_vpd_setup.sql @@ -341,22 +341,34 @@ BEGIN RETURN '1 = 1'; ELSIF r.rule_type = 'MY_DEPT' THEN v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE')); - append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')'); + IF v_column IS NOT NULL THEN + append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')'); + END IF; ELSIF r.rule_type = 'SELF' THEN v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO')); - append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')'); + IF v_column IS NOT NULL THEN + append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')'); + END IF; ELSIF r.rule_type = 'DEPT' THEN v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE')); - append_or(v_column || ' = ' || quote_literal(r.rule_value)); + IF v_column IS NOT NULL THEN + append_or(v_column || ' = ' || quote_literal(r.rule_value)); + END IF; ELSIF r.rule_type = 'EMP_NO' THEN v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO')); - append_or(v_column || ' = ' || quote_literal(r.rule_value)); + IF v_column IS NOT NULL THEN + append_or(v_column || ' = ' || quote_literal(r.rule_value)); + END IF; ELSIF r.rule_type = '=' THEN v_column := safe_column(p_schema, p_object, r.rule_column); - append_or('TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value)); + IF v_column IS NOT NULL THEN + append_or('TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value)); + END IF; ELSIF r.rule_type IN ('!=', '<>') THEN v_column := safe_column(p_schema, p_object, r.rule_column); - append_or('TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value)); + IF v_column IS NOT NULL THEN + append_or('TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value)); + END IF; END IF; END; END LOOP; 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 86a3aba..fc35bff 100644 --- a/sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql +++ b/sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql @@ -18,6 +18,8 @@ CREATE OR REPLACE FUNCTION cb_agent_doc_vpd_filter( ) RETURN VARCHAR2 AUTHID DEFINER AS + -- This function is intentionally whitelist-first: + -- no matched stored row rule means no rows are visible. v_user_id NUMBER; v_target VARCHAR2(128); v_predicate VARCHAR2(32767); @@ -104,23 +106,37 @@ BEGIN IF r.rule_type = 'ALL' THEN RETURN '1 = 1'; ELSIF r.rule_type = 'MY_DEPT' THEN + -- NULL rule_column keeps backward compatibility with the demo seed. v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE')); - append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')'); + IF v_column IS NOT NULL THEN + append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')'); + END IF; ELSIF r.rule_type = 'SELF' THEN + -- NULL rule_column keeps backward compatibility with the demo seed. v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO')); - append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')'); + IF v_column IS NOT NULL THEN + append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')'); + END IF; ELSIF r.rule_type = 'DEPT' THEN v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE')); - append_or(v_column || ' = ' || quote_literal(r.rule_value)); + IF v_column IS NOT NULL THEN + append_or(v_column || ' = ' || quote_literal(r.rule_value)); + END IF; ELSIF r.rule_type = 'EMP_NO' THEN v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO')); - append_or(v_column || ' = ' || quote_literal(r.rule_value)); + IF v_column IS NOT NULL THEN + append_or(v_column || ' = ' || quote_literal(r.rule_value)); + END IF; ELSIF r.rule_type = '=' THEN v_column := safe_column(p_schema, p_object, r.rule_column); - append_or('TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value)); + IF v_column IS NOT NULL THEN + append_or('TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value)); + END IF; ELSIF r.rule_type IN ('!=', '<>') THEN v_column := safe_column(p_schema, p_object, r.rule_column); - append_or('TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value)); + IF v_column IS NOT NULL THEN + append_or('TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value)); + END IF; END IF; END; END LOOP; diff --git a/sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql b/sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql new file mode 100644 index 0000000..0261f45 --- /dev/null +++ b/sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql @@ -0,0 +1,191 @@ +-- ============================================================ +-- 27_agent_ords_security_dynamic_vpd_filter_test.sql +-- Unit-style checks for cb_agent_doc_vpd_filter. +-- +-- Run after: +-- @sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql +-- ============================================================ +WHENEVER SQLERROR EXIT SQL.SQLCODE +SET ECHO OFF +SET FEEDBACK ON +SET DEFINE OFF +SET SERVEROUTPUT ON + +PROMPT === Preparing dynamic VPD filter unit test data === + +BEGIN + cb_agent_ctx_pkg.clear_user; + + DELETE FROM cb_permission_rule + WHERE perm_id BETWEEN 456000 AND 456999; + + DELETE FROM cb_permission + WHERE perm_id BETWEEN 456000 AND 456999; + + DELETE FROM cb_user_role + WHERE role_id BETWEEN 456000 AND 456999; + + DELETE FROM cb_app_role + WHERE role_id BETWEEN 456000 AND 456999; + + DELETE FROM cb_app_user + WHERE user_id BETWEEN 456000 AND 456999; + + INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active) + VALUES (456101, 'rule_test_hr', 'E456101', 'HR', 'N', 'Y'); + + INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active) + VALUES (456102, 'rule_test_self', 'E2001', 'FIN', 'N', 'Y'); + + INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active) + VALUES (456103, 'rule_test_all', 'E456103', 'HQ', 'Y', 'Y'); + + INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active) + VALUES (456104, 'rule_test_eq', 'E456104', 'QA', 'N', 'Y'); + + INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active) + VALUES (456105, 'rule_test_ne', 'E456105', 'QA', 'N', 'Y'); + + INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active) + VALUES (456106, 'rule_test_bad_column', 'E456106', 'QA', 'N', 'Y'); + + INSERT INTO cb_app_role(role_id, role_name) VALUES (456101, 'RULE_TEST_MY_DEPT'); + INSERT INTO cb_app_role(role_id, role_name) VALUES (456102, 'RULE_TEST_SELF'); + INSERT INTO cb_app_role(role_id, role_name) VALUES (456103, 'RULE_TEST_ALL'); + INSERT INTO cb_app_role(role_id, role_name) VALUES (456104, 'RULE_TEST_EQ'); + INSERT INTO cb_app_role(role_id, role_name) VALUES (456105, 'RULE_TEST_NE'); + INSERT INTO cb_app_role(role_id, role_name) VALUES (456106, 'RULE_TEST_BAD_COLUMN'); + + INSERT INTO cb_user_role(user_id, role_id) VALUES (456101, 456101); + INSERT INTO cb_user_role(user_id, role_id) VALUES (456102, 456102); + INSERT INTO cb_user_role(user_id, role_id) VALUES (456103, 456103); + INSERT INTO cb_user_role(user_id, role_id) VALUES (456104, 456104); + INSERT INTO cb_user_role(user_id, role_id) VALUES (456105, 456105); + INSERT INTO cb_user_role(user_id, role_id) VALUES (456106, 456106); + + INSERT INTO cb_permission(perm_id, role_id, target_name, action_name) + VALUES (456101, 456101, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); + + INSERT INTO cb_permission(perm_id, role_id, target_name, action_name) + VALUES (456102, 456102, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); + + INSERT INTO cb_permission(perm_id, role_id, target_name, action_name) + VALUES (456103, 456103, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); + + INSERT INTO cb_permission(perm_id, role_id, target_name, action_name) + VALUES (456104, 456104, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); + + INSERT INTO cb_permission(perm_id, role_id, target_name, action_name) + VALUES (456105, 456105, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); + + INSERT INTO cb_permission(perm_id, role_id, target_name, action_name) + VALUES (456106, 456106, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); + + INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value) + VALUES (456101, 456101, NULL, 'MY_DEPT', NULL); + + INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value) + VALUES (456102, 456102, NULL, 'SELF', NULL); + + INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value) + VALUES (456103, 456103, NULL, 'ALL', NULL); + + INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value) + VALUES (456104, 456104, 'DOC_ID', '=', '1'); + + INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value) + VALUES (456105, 456105, 'DEPT_CODE', '!=', 'HR'); + + INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value) + VALUES (456106, 456106, 'NO_SUCH_COLUMN', '=', 'x'); + + COMMIT; +END; +/ + +PROMPT === Running dynamic VPD filter unit checks === + +DECLARE + PROCEDURE fail(p_label IN VARCHAR2, p_expected IN VARCHAR2, p_actual IN VARCHAR2) IS + BEGIN + RAISE_APPLICATION_ERROR( + -20456, + p_label || ' expected [' || p_expected || '] actual [' || p_actual || ']' + ); + END; + + PROCEDURE assert_equals( + p_label IN VARCHAR2, + p_user_id IN NUMBER, + p_expected IN VARCHAR2 + ) IS + v_actual VARCHAR2(32767); + BEGIN + IF p_user_id IS NULL THEN + cb_agent_ctx_pkg.clear_user; + ELSE + cb_agent_ctx_pkg.set_user(p_user_id); + END IF; + + v_actual := cb_agent_doc_vpd_filter('ADMIN', 'CB_V_SEARCH_DOCUMENTS'); + + IF v_actual != p_expected THEN + fail(p_label, p_expected, v_actual); + END IF; + + DBMS_OUTPUT.PUT_LINE('PASS ' || p_label || ': ' || v_actual); + END; + + PROCEDURE assert_contains( + p_label IN VARCHAR2, + p_user_id IN NUMBER, + p_expected IN VARCHAR2 + ) IS + v_actual VARCHAR2(32767); + BEGIN + cb_agent_ctx_pkg.set_user(p_user_id); + v_actual := cb_agent_doc_vpd_filter('ADMIN', 'CB_V_SEARCH_DOCUMENTS'); + + IF INSTR(v_actual, p_expected) = 0 THEN + fail(p_label, p_expected, v_actual); + END IF; + + DBMS_OUTPUT.PUT_LINE('PASS ' || p_label || ': ' || v_actual); + END; +BEGIN + assert_equals('NO_CONTEXT_DENIES', NULL, '1 = 0'); + assert_contains('MY_DEPT_DEFAULT_COLUMN', 456101, 'DEPT_CODE = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')'); + assert_contains('SELF_DEFAULT_COLUMN', 456102, 'OWNER_EMP_NO = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')'); + assert_equals('ALL_ALLOWS', 456103, '1 = 1'); + assert_contains('EQUALS_RULE', 456104, 'TO_CHAR(DOC_ID) = ''1'''); + assert_contains('NOT_EQUALS_RULE', 456105, 'TO_CHAR(DEPT_CODE) <> ''HR'''); + assert_equals('BAD_COLUMN_DENIES', 456106, '1 = 0'); + cb_agent_ctx_pkg.clear_user; +END; +/ + +PROMPT === Cleaning dynamic VPD filter unit test data === + +BEGIN + cb_agent_ctx_pkg.clear_user; + + DELETE FROM cb_permission_rule + WHERE perm_id BETWEEN 456000 AND 456999; + + DELETE FROM cb_permission + WHERE perm_id BETWEEN 456000 AND 456999; + + DELETE FROM cb_user_role + WHERE role_id BETWEEN 456000 AND 456999; + + DELETE FROM cb_app_role + WHERE role_id BETWEEN 456000 AND 456999; + + DELETE FROM cb_app_user + WHERE user_id BETWEEN 456000 AND 456999; + + COMMIT; +END; +/ + +PROMPT === Dynamic VPD filter unit checks passed ===