# 06 · ORDS 기반 Agent 사용자 식별·권한 매핑 및 VPD/DDS 데이터 접근 통제 설계

> 목적: ORDS 기반 Agent 요청의 사용자 식별, 권한 매핑, Oracle VPD/DDS 기반 데이터 접근 통제, 사번/부서코드 기반 행 단위 필터링 구조 정의
> 범위: 권한 요청/승인 API 구현은 제외. DBA 수작업 등록을 전제로 권한 매핑 테이블 설계, 보안 정책 적용 방식, ADB 실행 검증 예제 포함
> 용어: "Deep Security"는 Oracle **Deep Data Security(DDS)** 로 표기

---

## 목차

- [1. 설계 개요 및 적용 범위](#1-설계-개요-및-적용-범위)
- [2. 사용자 식별 및 권한 매핑 구조](#2-사용자-식별-및-권한-매핑-구조)
- [3. 권한 매핑 테이블 설계](#3-권한-매핑-테이블-설계)
- [4. VPD 기반 접근 통제 설계](#4-vpd-기반-접근-통제-설계)
- [5. DDS 기반 접근 통제 설계](#5-dds-기반-접근-통제-설계)
- [6. VPD/DDS 비교 및 적용 기준](#6-vpddds-비교-및-적용-기준)
- [7. ADB 실행 검증](#7-adb-실행-검증)
- [8. 결과 화면 및 운영 확인 항목](#8-결과-화면-및-운영-확인-항목)

문서 흐름은 VPD와 DDS를 먼저 독립적으로 설명한 뒤, 비교 섹션에서 적용 기준을 정리하는 방식으로 구성한다. 고객 설명 시에는 1~3장에서 공통 구조를 먼저 설명하고, 4장은 VPD 경로, 5장은 DDS 경로, 6장은 적용 선택 기준으로 연결한다.

---

## 1. 설계 개요 및 적용 범위

본 구조의 기본 원칙은 Agent 계층에서 데이터 권한을 판정하지 않는 것이다. ORDS는 요청자를 식별하고, DB는 보호 객체(VIEW/TABLE)에 연결된 정책으로 허용된 행(row)과 컬럼(column)만 반환한다.

요청 처리 흐름은 다음과 같다. Agent 요청은 ORDS를 통과하고, ORDS는 `Authorization` 헤더 또는 DB 접속 사용자로 요청자를 식별한다. 이후 실제 데이터 제한은 DB 내부 정책에서 수행한다. 기본 적용 경로는 Bearer Key 기반 VPD이며, DDS는 DDS `END USER` 또는 지원 드라이버의 Security Context 전파가 가능한 경우의 확장 경로로 구분한다.

![아키텍처 개요 및 적용 경로](assets/agent-ords-security-main-trunk.svg)

요청 처리 절차:

1. Agent가 ORDS API 호출
2. ORDS가 요청 수신
3. ORDS 또는 DB가 요청자를 식별
4. DB Session Context 또는 DDS 보안 Identity에 권한 기준 생성
5. Agent 요청은 보호 객체(VIEW/TABLE) 조회
6. VPD가 행(row) 정책 적용. Redaction은 민감 컬럼 값을 NULL/마스킹하고, DDS는 컬럼(column) 허용/제외 적용
7. 허용된 결과만 반환

구성 요소:

| 구분 | 선택지 | 역할 |
|---|---|---|
| 사용자 식별 | DB User | Oracle 로그인 계정으로 요청자 식별 |
| 사용자 식별 | Bearer Key | ORDS Authorization 헤더의 키로 내부 사용자 식별 |
| 사용자 식별 | DDS END USER | DDS 보안 사용자로 식별 |
| 정책 적용 | VPD | `SYS_CONTEXT`, `p_object`, 권한 테이블로 행(row) 제한 |
| 정책 적용 | DDS | `DATA ROLE`, `DATA GRANT`로 행(row)/컬럼(column)/작업(action) 제한 |
| 컬럼 처리 | Redaction 또는 DDS 컬럼 Grant | Redaction은 값을 NULL/마스킹, DDS는 컬럼 허용/제외 |

컬럼 처리 정리:

| 방식 | 역할 | 이 문서의 설명 기준 |
|---|---|---|
| VPD 기본 정책 | 행(row) 조건 반환 | 컬럼 통제 수단으로 설명하지 않음 |
| Oracle Data Redaction | 컬럼 값을 NULL, 부분 마스킹, 정규식 마스킹 값으로 변환 | 컬럼 Grant가 아니라 값 마스킹/NULL 처리 |
| DDS DATA GRANT | `AS SELECT (ALL COLUMNS EXCEPT ...)`로 컬럼 허용/제외 | 컬럼 통제로 설명 |
| Column-level VPD 옵션 | `sec_relevant_cols`, `sec_relevant_cols_opt` 옵션이 있으나 운영 권한 모델로는 혼동 가능 | 본 구조에서는 사용하지 않고 설명 범위에서도 제외 |

따라서 이 문서에서의 결론은 아래와 같다.

```text
VPD = 행(row) 통제
Redaction = 컬럼 값 NULL/마스킹
DDS = 행(row) + 컬럼(column) + 작업(action) 통제
```

주요 구분:

> DB에 접속한 사용자와 실제 권한을 적용할 사용자는 다를 수 있다. 특히 ORDS 공통 계정으로 접속하는 구조에서는 `DB user != key user`다.

설명 순서:

1. 사용자 식별 방식과 권한 매핑 구조 정의
2. Bearer Key 기반 VPD 경로 설명
3. VPD에서 행(row) 통제와 Redaction 컬럼 값 마스킹 구분
4. DDS에서 END USER, DATA ROLE, DATA GRANT 기반 통제 설명
5. VPD와 DDS의 적용 조건, 운영 차이, 검증 결과 비교

보호 객체 범위:

> 본 예제는 API 노출 지점과 우회 차단 구조를 설명하기 위해 VIEW를 주로 사용한다. 단, 지원 범위가 VIEW로 한정되는 것은 아니다. VPD와 DDS는 보호 정책을 VIEW 또는 TABLE 같은 DB 객체에 연결할 수 있다. VIEW는 원본 TABLE을 숨기고 API용 노출 객체를 분리하기 위한 래퍼 패턴이다.

### 1.1 전체 처리 흐름 요약

다음 세 개의 다이어그램은 전체 구조를 요약한다. 이후 상세 섹션에서 동일한 흐름을 SQL, 정책, 검증 결과 기준으로 설명한다.

![두 가지 사용자 식별 시나리오 화면](assets/agent-ords-security-two-scenarios.svg)

| 확인 항목 | 의미 |
|---|---|
| 시나리오 1 - DB User 기반 | Oracle 접속 계정으로 요청자를 식별 |
| 시나리오 2 - Bearer Key 기반 | ORDS `Authorization` 헤더의 키로 내부 사용자를 식별 |
| 공통 하단 흐름 | 식별 이후에는 DB 보안 정책이 보호 객체(VIEW/TABLE)에 적용 |
| 기본 방향 | ORDS Bearer Key 기반은 VPD + Redaction을 기본 적용 |

![권한 등록 매핑 화면](assets/agent-ords-security-permission-mapping-screen.svg)

| 확인 항목 | 의미 |
|---|---|
| `APP_USER` | 실제 권한을 적용할 내부 사용자 |
| `AGENT_BEARER_KEY` | Bearer Key Hash로 내부 사용자를 찾는 매핑 |
| `USER_ROLE` | 사용자가 어떤 role을 갖는지 |
| `PERMISSION` | 어떤 보호 객체를 조회할 수 있는지 |
| `PERMISSION_RULE` | 보호 객체 내 허용 행(row) 조건 |
| `p_object` | Oracle이 VPD 함수에 넘겨주는 현재 조회 객체 이름 |

![권한 미부여 및 차단 결과 화면](assets/agent-ords-security-no-permission-screen.svg)

| 확인 항목 | 의미 |
|---|---|
| 권한 매핑 없음 | 보호 객체 DB 권한은 있어도 VPD 조건이 통과하지 못해 0건 |
| DB 권한 미부여 | VPD 판단 전 객체가 보이지 않아 `ORA-00942` 또는 `ORA-01031` |
| Bearer 헤더 없음 | ORDS Handler 필수값 차단. 예: `ORA-20101` |
| 잘못된 Key | Key Hash 매핑 실패. 예: `ORA-20002` |

### 1.2 운영 권한 등록 항목

권한 요청/승인 API 구현은 본 범위에서 제외한다. 단, DBA가 수작업으로 등록하더라도 아래 항목은 사전에 정의되어야 한다.

표의 객체명은 설계 설명용 논리명이다. ADB 실행 예제는 기존 객체와의 충돌을 방지하기 위해 `CB_APP_USER`, `CB_AGENT_BEARER_KEY`처럼 `CB_*` 접두어를 사용한다. 또한 `db_username`은 DB User 기반 시나리오를 위한 설계 컬럼이며, Bearer Key 중심 실행 예제의 `CB_APP_USER`에는 포함하지 않았다.

| 등록 영역 | 필요한 값 | 저장 또는 적용 위치 | 의미 |
|---|---|---|---|
| 내부 사용자 | `user_id`, 사용자명, 사번, 부서코드, 활성 여부 | `APP_USER` | 실제 권한 적용 대상 |
| DB User 매핑 | Oracle username | `APP_USER.db_username` 또는 별도 매핑 컬럼 | DB User 기반 시나리오에서 사용자 식별 |
| Bearer Key 매핑 | Key Hash, Key Prefix, 만료일, 회수일, 활성 여부 | `AGENT_BEARER_KEY` | ORDS Authorization 헤더의 키를 내부 사용자로 변환 |
| 역할 | role id, role name | `APP_ROLE` | 권한 그룹 |
| 사용자-역할 | user id, role id | `USER_ROLE` | 사용자가 어떤 역할을 갖는지 |
| 보호 객체 권한 | target name, action | `PERMISSION` | 예: `CB_V_SEARCH_DOCUMENTS`, `SELECT` |
| 행 규칙 | rule type, rule value | `PERMISSION_RULE` | 예: `MY_DEPT=HR`, `SELF`, `ALL` |
| 민감 컬럼 처리 | 컬럼 값 표시 허용 여부 | `APP_USER.can_read_contents`, Redaction 정책 | 예: `contents` 원문 또는 NULL |
| 보호 객체 목록 | schema, object name, enabled | 보호 객체 목록 테이블 또는 배포 설정 | `DBMS_RLS.ADD_POLICY` 반복 등록 기준 |
| DDS 사용자 | end user name | `CREATE END USER` | DDS 직접 사용자 식별 |
| DDS 역할/권한 | DATA ROLE, DATA GRANT, Predicate, Column List | `CREATE DATA ROLE`, `CREATE DATA GRANT` | DDS 선언형 행/컬럼 권한 |

운영 등록 항목은 두 축으로 구분한다.

```text
요청 주체 식별
  -> DB User 또는 Bearer Key로 내부 사용자 식별

데이터 접근 범위
  -> role / permission / permission_rule 또는 DDS DATA GRANT로 결정
```

### 1.3 권한 판정 절차 및 차단 유형

VPD의 동적 권한 판정은 `p_object`와 `permission.target_name` 매핑으로 수행된다.

```text
Agent 요청
  -> ORDS가 사용자 식별
  -> DB Session Context에 USER_ID / EMP_NO / DEPT_CODE 저장
  -> 보호 객체 조회
  -> Oracle이 VPD 함수에 p_object 전달
  -> permission.target_name = p_object 인 권한 확인
  -> permission_rule이 행(row)의 컬럼 값과 일치하는지 확인
  -> 조건을 충족하는 행(row)만 반환
```

| 확인 지점 | 통과 조건 | 실패 결과 |
|---|---|---|
| ORDS 헤더 | `Authorization: Bearer <key>` 형식 | `ORA-20101` 또는 401/403 |
| Key 매핑 | Key Hash가 활성 사용자와 매핑 | `ORA-20002`, Context 초기화 |
| DB 객체 권한 | ORDS runtime schema가 보호 객체 조회 가능 | `ORA-00942` 또는 `ORA-01031` |
| VPD 객체 권한 | `permission.target_name = p_object` 존재 | 0건 |
| VPD 행 규칙 | `MY_DEPT`, `SELF`, `ALL` 규칙 통과 | 해당 행(row) 제외 |
| 민감 컬럼 처리 | 컬럼 표시 Flag 또는 DDS 컬럼 Grant 통과 | Redaction NULL 또는 컬럼 제외 |
| DDS Identity | `END USER` 또는 `EndUserSecurityContext` 전파 | DATA GRANT 미적용, `ORA-00942` 가능 |

따라서 차단 또는 미조회 결과는 발생 지점에 따라 의미가 다르다.

```text
권한 매핑 없음 = SQL은 실행되지만 결과 0건
DB 객체 권한 미부여 = 객체가 보이지 않거나 권한 오류
Bearer Key 문제 = ORDS/DB Package 단계에서 차단
DDS Context 없음 = DDS DATA GRANT가 적용될 사용자 Identity 없음
```

### 1.4 설계 결론

| 구분 | 결론 |
|---|---|
| 기본 권장안 | ORDS가 `Authorization: Bearer <key>`를 받고 DB 테이블에서 사용자를 매핑하는 구조는 **VPD + Redaction**을 기본 적용안으로 권장한다. |
| DDS 적용 조건 | DDS는 실제 사용자가 `END USER`로 접속하거나, 지원 드라이버 또는 호출 애플리케이션이 `EndUserSecurityContext`를 DB 호출 전에 전파할 수 있을 때 적합하다. |
| 유의 사항 | ORDS PL/SQL Handler가 키를 조회해 변수에 저장하는 것만으로는 DDS `END USER`가 되지 않는다. 이 경로는 ADB에서 차단 검증했다. |
| 운영 관리 | VPD/DDS 모두 객체별 적용이 필요하다. 대신 Dictionary View와 Inventory SQL로 적용 현황을 중앙 확인한다. |

### 1.5 요구사항 대응

| 요구사항 | 대응 방안 |
|---|---|
| Agent를 Oracle User처럼 권한 제어 | 가능하다. DB User 기반은 `SESSION_USER`로 매핑하고, ORDS 공통 계정 기반은 Bearer Key를 내부 사용자로 매핑한다. |
| 사번/부서코드로 검색 데이터 필터링 | VPD는 `SYS_CONTEXT`에 사번/부서코드를 저장하고 정책 함수가 행(row) 조건을 반환한다. DDS는 `DATA GRANT ... WHERE dept_code = ...`로 선언한다. |
| ORDS API 개발 위치 | ORDS Handler는 Authorization 헤더 필수값 확인과 키 전달을 담당한다. 실제 권한 판단은 DB Package, VPD 함수, DDS DATA GRANT에서 수행한다. |
| Bearer Key 기반 시나리오 | VPD 권장. Key User가 많고 권한이 자주 변경되면 테이블 기반 권한 관리가 운영상 유리하다. |
| DDS 사용 시나리오 | DDS `END USER` 또는 Driver-level `EndUserSecurityContext` 전파가 가능할 때 권장한다. 단순 ORDS Handler Key Lookup만으로는 부족하다. |
| 권한 현황 확인 | VPD는 `DBA_POLICIES`, Redaction View, 업무 권한 테이블을 확인한다. DDS는 `DBA_DATA_GRANTS`, `DBA_DATA_ROLE_GRANTS`, `DBA_DATA_ROLES`, `DBA_END_USERS`를 확인한다. |

### 1.6 검증 결과 요약

검증은 VPD, DDS, ORDS Handler 검증, 공통 인벤토리로 구분해 확인했다.

VPD + Redaction 검증:

| 항목 | 상태 | 근거 |
|---|---|---|
| Bearer Key -> Context 설정 | 실행 검증 완료 | `CB_ORDS`가 `cb_hr_key`, `cb_fin_key`, `cb_all_key`를 `CB_AGENT_CTX`에 저장 |
| 행(row) 제한 | 실행 검증 완료 | HR 3건, FIN 본인 1건, ALL 6건 |
| Redaction 값 마스킹/NULL | 실행 검증 완료 | `contents` 컬럼 값은 VPD가 아니라 Redaction이 처리. HR/FIN은 NULL, ALL은 원문 표시 |
| Key 오류 처리 | 실행 검증 완료 | Invalid Key는 `ORA-20002`, Context 초기화 후 0건 |
| 원본/권한 테이블 우회 차단 | 실행 검증 완료 | `CB_SEARCH_DOCUMENTS`, `CB_APP_USER`, `CB_AGENT_BEARER_KEY` 직접 조회는 `ORA-00942` |

DDS 검증:

| 항목 | 상태 | 근거 |
|---|---|---|
| Direct END USER 접속 | 실행 검증 완료 | `cb_dds_hr`, `cb_dds_fin`, `cb_dds_all`, `cb_dds_none`로 접속 |
| DATA GRANT 행(row) 제한 | 실행 검증 완료 | HR 3건, FIN 2건, ALL 6건 |
| DATA GRANT 컬럼(column) 허용/제외 | 실행 검증 완료 | HR/FIN은 `ALL COLUMNS EXCEPT contents`, ALL은 전체 컬럼 |
| 권한 없는 END USER | 실행 검증 완료 | `cb_dds_none`은 보호 객체 조회 시 `ORA-00942` |
| 원본/VPD 객체 우회 차단 | 실행 검증 완료 | DDS End User가 원본 TABLE 또는 VPD 보호 객체 직접 조회 시 `ORA-00942` |

ORDS Handler 검증:

| 항목 | 상태 | 근거 |
|---|---|---|
| Mandatory Bearer 헤더 | 실행 검증 완료 | 헤더가 없으면 `ORA-20101` |
| VPD Bearer Handler | 실행 검증 완료 | Handler Package가 Key를 Context로 저장하고 HR 행(row) 3건 반환 |
| DDS Bearer 단독 매핑 Handler | 차단 검증 완료 | Key를 `cb_dds_hr`로 매핑해도 `dds_context_username=null`, `ORA-00942`, `EXPECTED_BLOCKED` |
| 실제 ORDS HTTP Endpoint 호출 | 별도 환경 필요 | ADB에는 ORDS Package와 Handler 정의를 구성했다. 외부 URL 호출은 ORDS URL/인증 설정 확인 후 수행 |

공통 확인:

| 항목 | 상태 | 근거 |
|---|---|---|
| VPD/Redaction Inventory | 실행 검증 완료 | `DBA_POLICIES`, `REDACTION_POLICIES`에서 정책 확인 |
| DDS Inventory | 실행 검증 완료 | `DBA_DATA_GRANTS`, `DBA_DATA_ROLE_GRANTS`에서 DATA GRANT와 END USER 매핑 확인 |
| DDS Driver-level Security Context | 별도 구성 필요 | IAM/DB Access Token 및 지원 드라이버 설정 필요. 문서에는 공식 API 기준 경로를 설명 |

### 1.7 권장 구현 구조

현재 요구사항의 주 경로는 아래 흐름이다.

```text
Agent
  -> ORDS Endpoint
  -> Authorization 헤더 필수 확인
  -> Bearer Key Hash 조회
  -> 내부 user_id / 사번 / 부서코드 / 컬럼 권한 식별
  -> DB Session Context reset + set
  -> 보호 객체(VIEW/TABLE) 조회
  -> VPD 행(row) 제한 + Redaction 값 NULL/마스킹
  -> 허용된 결과만 반환
```

역할을 나누면 아래와 같다.

| 계층 | 맡는 일 | 맡지 않는 일 |
|---|---|---|
| Agent | ORDS API 호출, `Authorization: Bearer <key>` 전달 | 행/컬럼 권한 판단 |
| ORDS Handler | 헤더 필수값 확인, 키를 DB Package로 전달, 오류를 HTTP 응답으로 변환 | 업무 권한 규칙 직접 구현 |
| DB Package | Key Hash 검증, 내부 사용자 조회, `SYS_CONTEXT` 값 reset/set | 보호 객체별 행 조건을 직접 SQL에 붙임 |
| VPD Function | 보호 객체 이름과 사용자 Context로 행 제한 조건 생성 | Bearer Key 원문 저장 |
| Redaction | 컬럼 권한에 따라 민감 컬럼 NULL 처리 | 행 제한 |
| Inventory SQL | VPD, Redaction, DDS 적용 현황 확인 | 업무 승인 사유 관리 |

DDS는 아래 조건이 충족될 때 같은 구조의 확장 경로로 설명한다.

```text
DDS END USER 직접 접속
  또는
지원 드라이버 또는 호출 애플리케이션이 EndUserSecurityContext를 DB 호출 전에 Attach
  -> DATA ROLE 확인
  -> DATA GRANT 적용
  -> 허용된 행(row)/컬럼(column)/작업(action)만 반환
```

### 1.8 실행 검증 완료 기준

아래 결과가 나오면 현재 요구사항의 실행 가능한 예제로 볼 수 있다.

VPD 완료 기준:

| 검증 | 기대 결과 |
|---|---|
| Context 없음 | 0건 |
| 잘못된 Bearer Key | `ORA-20002`, Context 초기화 후 0건 |
| `cb_hr_key` | HR 행(row) 3건, `contents` NULL |
| `cb_fin_key` | 본인 행(row) 1건, `contents` NULL |
| `cb_all_key` | 전체 행(row) 6건, `contents` 원문 표시 |
| 원본 TABLE 직접 조회 | `ORA-00942` 또는 직접 권한 미부여 |

DDS 완료 기준:

| 검증 | 기대 결과 |
|---|---|
| DDS `cb_dds_hr` | HR 행(row) 3건, `contents` NULL |
| DDS `cb_dds_fin` | FIN 행(row) 2건, `contents` NULL |
| DDS `cb_dds_all` | 전체 행(row) 6건, `contents` 원문 표시 |
| DDS `cb_dds_none` | `ORA-00942` |

ORDS Handler 검증 완료 기준:

| 검증 | 기대 결과 |
|---|---|
| Bearer 헤더 없음 | `ORA-20101` |
| VPD Handler `cb_hr_key` | HR 행(row) 3건, `contents` NULL |
| ORDS Handler의 DDS Bearer 단독 매핑 검증 | `dds_context_username=null`, `ORA-00942`, `EXPECTED_BLOCKED` |

공통 완료 기준:

| 검증 | 기대 결과 |
|---|---|
| 중앙 권한 확인 | `DBA_POLICIES`, `REDACTION_POLICIES`, `DBA_DATA_GRANTS`, `DBA_DATA_ROLE_GRANTS`에서 정책 확인 |

### 1.9 VPD 적용 단위 기준

VPD는 보호할 TABLE 또는 VIEW에 **객체별로** 연결한다. `DBMS_RLS.ADD_POLICY`의 `object_name`은 실제 객체 이름을 받는 값이다.

| 검토 항목 | 기준 |
|---|---|
| schema 전체에 한 번에 VPD를 걸 수 있는가 | 아니다. `ADD_POLICY` 1회는 보호 객체 1개에 적용된다. |
| `object_name => '*'` 또는 `%` 표현이 가능한가 | 아니다. wildcard로 전체 TABLE/VIEW를 지정하는 방식이 아니다. |
| `statement_types`에 여러 SQL 작업을 지정하면 전체 객체 적용인가 | 아니다. `SELECT, INSERT, UPDATE, DELETE`처럼 적용할 SQL 작업 종류를 지정하는 값이다. 보호 객체 wildcard가 아니다. |
| 객체가 많으면 어떻게 하는가 | 보호 객체 목록을 관리하고, 목록을 루프 돌면서 `ADD_POLICY`를 객체별로 생성한다. |
| 같은 정책 함수를 재사용할 수 있는가 | 가능하다. 정책 함수는 하나로 두고, `p_object` 값으로 현재 조회 객체를 구분한다. |
| 매핑 테이블에 권한이 없으면 어떻게 되는가 | VPD 조건이 통과하지 못하므로 해당 보호 객체 조회 결과는 0건이다. |

정리:

```text
전체 적용 = Wildcard 1회 등록
아님

전체 적용 = 보호 객체 목록 전체를 돌며 객체별 ADD_POLICY 등록
```

<details>
<summary><strong>운영 검토 항목</strong> - 적용 단위, 권한 조회, 누락 방지</summary>

| 검토 항목 | 기준 |
|---|---|
| Bearer Key만으로 DDS 사용자 권한까지 적용되는가 | ORDS Handler에서 Key를 읽는 것만으로는 적용되지 않는다. VPD는 `SYS_CONTEXT` 방식으로 가능하고, DDS는 `EndUserSecurityContext` 전파가 필요하다. |
| 권한 현황을 한눈에 볼 수 있는가 | 가능하다. VPD는 `DBA_POLICIES`와 업무 권한 테이블, DDS는 `DBA_DATA_GRANTS`, `DBA_DATA_ROLE_GRANTS`, `DBA_DATA_ROLES`, `DBA_END_USERS`로 확인한다. |
| VPD 정책을 모든 VIEW/TABLE에 한 번에 걸 수 있는가 | 아니다. `DBMS_RLS.ADD_POLICY`는 보호 객체 하나를 지정한다. `object_name => '*'` 같은 Wildcard 등록은 하지 않는다. 여러 객체는 같은 정책 함수를 재사용하되 객체별로 등록하거나 스크립트로 일괄 생성한다. |
| DDS도 객체별로 권한을 선언해야 하는가 | `DATA GRANT`는 보호 객체, 역할, 행/컬럼 조건 단위로 선언한다. 적용은 선언 단위지만 Dictionary View로 중앙 조회가 가능하다. |
| 객체가 많아지면 누락 위험은 어떻게 줄이는가 | 보호 객체 목록 테이블 또는 설정 파일을 기준으로 VPD/DDS 등록 스크립트를 생성하고, Dictionary 조회 결과를 검증 단계에 포함한다. |

</details>

<details>
<summary><strong>오류 코드 상세</strong> - ORA 코드 해석과 정상 차단 여부</summary>

| 오류 코드 | 이 문서에서의 의미 | 정상 차단 여부 | 확인 방법 |
|---|---|---|---|
| `ORA-00942` | table 또는 view가 없거나, 현재 사용자에게 객체가 보이지 않음 | DDS에서 `DATA GRANT`가 없거나 원본 TABLE 권한을 숨긴 경우 정상 차단 | `DBA_DATA_GRANTS`, `DBA_DATA_ROLE_GRANTS`, `DBA_POLICIES`, 직접 Grant 여부 확인 |
| `ORA-20002` | 잘못되었거나 만료된 Bearer Key | 정상 차단. `RAISE_APPLICATION_ERROR`로 만든 사용자 정의 오류 | `agent_bearer_key`의 Hash, Active, Revoked, Expires 상태 확인 |
| `ORA-20101` | `Authorization: Bearer <key>` 헤더가 없거나 형식이 틀림 | 정상 차단. ORDS Handler Package에서 만든 사용자 정의 오류 | ORDS 요청 헤더와 Handler Bind Variable 확인 |
| `ORA-01031` | 권한 부족 | 우회 시도 테스트에서는 정상 차단 | 사용자가 `DBMS_SESSION`, 정책 변경, 원본 객체 접근 권한을 갖고 있는지 확인 |
| `ORA-02019` | 원격 DB 연결 식별자 또는 DB Link 접근 불가 | 원격 원본 직접 조회 차단 테스트에서는 정상 차단 | DB Link 존재 여부, 직접 Grant 여부, 네트워크/원격 접속 설정 확인 |

주의할 점:

| 구분 | 설명 |
|---|---|
| `ORA-00942` | 실제로 객체가 없을 때도 발생하고, 권한 미부여로 객체가 숨겨질 때도 발생한다. 보안 테스트에서는 Dictionary 조회 결과와 함께 해석해야 한다. |
| `ORA-20000`~`ORA-20999` | Oracle 표준 오류가 아니라 개발자가 `RAISE_APPLICATION_ERROR`로 정의하는 사용자 오류 범위다. 이 문서의 `ORA-20002`, `ORA-20101`이 여기에 해당한다. |
| 정상 차단 오류 | 보안 검증에서는 실패가 아니라 “우회가 막혔다”는 증거다. 단, 운영 장애 분석에서는 같은 코드라도 발생 위치와 사용자, 대상 객체를 함께 봐야 한다. |

</details>

<details>
<summary><strong>운영 책임 상세</strong> - 누가 무엇을 관리하는지</summary>

| 영역 | 담당 역할 | 관리 항목 |
|---|---|---|
| Agent | API 호출 주체 | Bearer Key 전달, 요청 Parameter 구성 |
| ORDS/API | API Gateway 및 Handler | Mandatory `Authorization` 헤더 확인, Handler Package 호출, 오류 응답 변환 |
| DB 보안 Package | Key 사용자 식별 | Key Hash 조회, 사용자 활성 상태 확인, Session Context reset/set |
| VPD/Redaction | 데이터 제한 | 행 제한 정책, 민감 컬럼 처리, 보호 객체별 정책 연결 |
| DDS | 선언형 데이터 권한 | END USER, DATA ROLE, DATA GRANT, 컬럼 Grant |
| DBA/Security | 보안 객체 등록 | DB User, Role, Grant, VPD Policy, DDS Object, Inventory 점검 |
| 업무 승인 관리 | 권한 요청/승인 이력 | 요청자, 승인자, 사유, ticket, 만료일, 회수 이력 |

권한 요청/승인 API는 본 범위에서 제외하지만, 테이블 설계에는 이력을 남길 자리를 둔다. 예를 들어 `permission_request`, `permission_approval`, `agent_bearer_key`의 `issued_at`, `expires_at`, `revoked_at` 같은 컬럼이 그 역할이다.

</details>

<details>
<summary><strong>성능·감사·운영 체크리스트</strong> - 적용 후 점검 항목</summary>

| 구분 | 체크 항목 |
|---|---|
| Index | `agent_bearer_key(key_hash)`, `user_role(user_id, role_id)`, `permission(role_id, target_name, action_name)`, `permission_rule(perm_id, rule_type, rule_value)` |
| 업무 컬럼 | 행 제한에 쓰는 `dept_code`, `owner_emp_no`는 검색 대상 테이블 또는 VIEW 기반 TABLE에 Index 검토 |
| VPD 함수 | 함수 안에서 복잡한 업무 로직을 실행하지 말고, 권한 테이블을 확인하는 SQL 조건을 반환하는 구조 유지 |
| 권한 누락 방지 | 보호 객체 목록을 관리하고, `DBMS_RLS.ADD_POLICY` 또는 `DATA GRANT` 등록 결과를 Inventory SQL로 비교 |
| 우회 차단 | ORDS runtime schema와 Agent DB user에는 원본 TABLE, 권한 테이블 직접 조회 권한을 주지 않음 |
| key 관리 | key 원문 저장 금지, hash 저장, prefix만 표시, 만료/회수/교체 절차 운영 |
| 감사 로그 | request id, key id 또는 user id, ORDS endpoint, result count, 오류 코드, 실행 시각 기록 |
| 정기 점검 | `DBA_POLICIES`, `REDACTION_POLICIES`, `DBA_DATA_GRANTS`, `DBA_DATA_ROLE_GRANTS` 결과를 배포 전후 비교 |

</details>

---

## 2. 사용자 식별 및 권한 매핑 구조

이 장에서는 Agent 요청이 DB 권한 적용 대상으로 변환되는 방식을 정리한다. DB User 기반, Bearer Key 기반, DDS END USER 기반을 구분하고, 본 요구사항의 기본 경로는 `DB user != Key User` 구조임을 명확히 한다.

<details>
<summary><strong>상세 설명</strong> - DB User, Bearer Key, DDS END USER 식별 경로</summary>

### 2.1 DB User 기반

Agent 또는 ORDS가 Oracle 계정으로 DB에 접속하고, DB는 `SESSION_USER`를 기준으로 사용자를 식별한다.

| Oracle 계정 | 사번 | 부서 | 역할 |
|---|---|---|---|
| `AGENT_HR_001` | `E10234` | `HR` | HR 검색 |
| `AGENT_FIN_002` | `E20411` | `FIN` | FIN 검색 |

흐름:

1. Agent 또는 ORDS가 `AGENT_HR_001`로 DB 접속
2. DB가 `SYS_CONTEXT('USERENV', 'SESSION_USER')` 확인
3. `app_user`에서 DB 계정에 해당하는 내부 사용자 조회
4. 사용자-역할-권한 테이블에서 접근 범위 확인
5. 보호 객체(VIEW/TABLE) 조회 시 선택한 정책 적용
6. VPD 경로는 VPD가 행(row)을 제한하고, 민감 컬럼 값은 Redaction으로 NULL/마스킹 처리
7. 컬럼 자체를 제외하려면 VIEW/TABLE 설계 또는 DDS DATA GRANT 사용
8. DDS 경로는 DATA GRANT가 행(row)/컬럼(column)/작업(action)을 제한

DB User 기반은 설명과 감사가 단순하다. 다만 ORDS가 항상 같은 DB 계정으로 접속하면 DB 계정만으로 실제 사용자를 구분할 수 없다.

### 2.2 Bearer Key 기반

ORDS가 공통 DB 계정으로 접속하는 경우에는 Authorization 헤더의 Bearer Key로 실제 사용자를 식별한다.

흐름:

1. Agent가 ORDS API 호출
2. ORDS 헤더에 `Authorization: Bearer <key>` 포함
3. ORDS Handler가 Bearer Key 필수 여부 확인
4. Key가 없거나 일치하지 않으면 401/403 또는 사용자 정의 오류 반환
5. Key Hash 계산
6. `agent_bearer_key` 테이블에서 내부 사용자 조회
7. 내부 사용자의 `USER_ID`, 사번, 부서코드, 컬럼 허용 Flag를 DB 세션에 저장
8. 보호 객체(VIEW/TABLE) 조회
9. VPD가 허용 행(row)만 반환
10. Redaction이 컬럼 허용 Flag를 보고 민감 컬럼을 NULL 처리하거나 원문 표시

Bearer Key 테이블 예:

```sql
CREATE TABLE agent_bearer_key (
  key_id       NUMBER PRIMARY KEY,
  user_id      NUMBER NOT NULL REFERENCES app_user(user_id),
  key_hash     VARCHAR2(128) NOT NULL UNIQUE,
  key_prefix   VARCHAR2(16),
  issued_at    DATE DEFAULT SYSDATE NOT NULL,
  expires_at   DATE,
  revoked_at   DATE,
  active       CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N'))
);
```

이 구조에서는 DB 계정이 하나여도 Key 사용자는 여러 명일 수 있다.

| 레벨 | 예 | 의미 |
|---|---|---|
| DB 접속 사용자 | `CB_ORDS` | ORDS가 DB에 접속할 때 쓰는 계정 |
| Key 사용자 | `agent_hr`, `agent_fin_self`, `agent_all` | Bearer Key가 가리키는 내부 사용자 |
| 권한 규칙 | HR 행(row), 본인 행(row), `contents` 컬럼 허용 여부 | 실제 데이터 제한 기준 |

따라서 Bearer Key 기반의 핵심은 아래와 같이 정리한다.

> `DB user != key user`. DB User는 접속 계정이고, Key User는 실제 권한 적용 대상이다.

### 2.3 DDS END USER 기반

DDS는 `END USER`, `DATA ROLE`, `DATA GRANT`로 권한을 선언한다.

| DDS 객체 | 역할 |
|---|---|
| `END USER` | 스키마를 소유하지 않는 보안 사용자 |
| `DATA ROLE` | 데이터 권한 묶음 |
| `DATA GRANT` | 어떤 VIEW/TABLE의 어떤 행(row)/컬럼(column)/작업(action)을 허용하는지 선언 |

Bearer Key가 많고 Key별 권한 변경이 잦은 구조라면 VPD의 권한 테이블 방식이 운영상 적합하다. 반대로 실제 사용자가 DDS `END USER`로 명확히 전파되고 정책을 DDL로 관리하려면 DDS가 적합하다.

DDS에는 확장 객체로 `APPLICATION IDENTITY`도 있다. 이 객체는 DDS 객체다. 다만 본 문서의 두 가지 사용자 식별 시나리오에서는 제외한다.

| DDS 확장 객체 | 의미 | 이 문서에서 제외한 이유 |
|---|---|---|
| `APPLICATION IDENTITY` | 특정 애플리케이션을 DB 안의 보안 Identity로 등록 | 현재 요청은 Agent/ORDS 자체 권한보다 사용자별 데이터 권한 설명이 핵심 |

`APPLICATION IDENTITY`는 애플리케이션 공통 권한이나 OAuth/OIDC 기반 애플리케이션 식별이 필요할 때 검토한다. Bearer Key가 가리키는 내부 사용자별 권한을 대체하는 개념은 아니다.

```sql
CREATE APPLICATION IDENTITY hcm_app
  MAPPED TO 'AZURE_CLIENT_ID=<client-id>';

GRANT DATA ROLE hcm_role
  TO hcm_app;
```

DDS에서 Bearer Key를 다룰 때는 두 단계를 구분해야 한다. ORDS Handler가 `Authorization` 헤더를 읽고 Key를 내부 사용자로 매핑하는 것은 가능하다. 그러나 그 매핑값이 자동으로 DDS `END USER`가 되지는 않는다.

![DDS Bearer Key 처리 검증](assets/agent-ords-security-dds-bearer-probe.svg)

ADB에서 ORDS Handler Package를 만들어 검증한 결과:

| 검증 항목 | 결과 |
|---|---|
| ORDS Handler에서 `Authorization` 헤더 필수 처리 | 가능 |
| Handler가 Key를 내부 사용자 또는 DDS End User 이름으로 매핑 | 가능 |
| 같은 Handler에서 VPD Context 설정 후 조회 | 가능 |
| 같은 Handler에서 DDS `EndUserSecurityContext` 생성/Attach | 검증 결과 생성/Attach되지 않음. DB Session 안의 PL/SQL Handler 단계는 Driver-level Attach 지점이 아님 |
| `CB_ORDS` Session에서 Key만 매핑하고 DDS 보호 객체 조회 | `ORA-00942`, `dds_context_username=null` |

따라서 단순 Bearer Key 테이블 Lookup 방식은 VPD 적용이 권장된다. DDS로 Bearer 기반 사용자 권한을 적용하려면 Key 검증 후 DDS `EndUserSecurityContext` Payload를 DB 호출 전에 전달하는 별도 드라이버 또는 호출 애플리케이션 경로가 필요하다.

| Bearer 유형 | DDS 적용 가능 여부 | 설명 |
|---|---|---|
| IAM/OIDC Bearer Token | 가능 | Token이 End-user Identity와 Role Claim을 담고 있고, 지원 드라이버 또는 호출 애플리케이션이 DDS Security Context Payload로 DB에 전달해야 함 |
| 애플리케이션 자체 API Key | 조건부 가능 | Key를 검증한 뒤 Local `END USER`와 Security Context Lookup Key로 변환하고, DB 호출 전에 Security Context를 Attach해야 함 |
| ORDS PL/SQL Handler의 단순 DB 테이블 Lookup Key | VPD 권장 | Handler 내부에서 Key를 찾는 것만으로 DDS End User Context가 생성되지 않음 |

DDS Security Context 전달 방식:

| 항목 | 확인 내용 |
|---|---|
| 전달 단위 | Oracle Client Driver가 `EndUserSecurityContext` Payload를 DB Connection Metadata로 전달 |
| Driver 지원 | JDBC, Python, ODP.NET |
| Python API | `oracledb.create_end_user_security_context()`, `connection.set_end_user_security_context()`, `connection.clear_end_user_security_context()` |
| IAM 사용자 | `end_user_token` + `database_access_token` 전달 |
| Local 사용자 | `(end_user_name, security_context_lookup_key)` + `database_access_token` 전달 |
| DB Pool User 권한 | `CREATE SESSION`, `CREATE END USER SECURITY CONTEXT` 필요 |
| DB에서 읽는 값 | `ORA_END_USER_CONTEXT.username`, Custom Context는 `ORA_END_USER_CONTEXT.<schema>.<context>.<attr>` |

즉, Bearer Key를 DDS에 적용하려면 아래 변환이 필요하다.

```text
Authorization: Bearer <api-key>
  -> 호출 애플리케이션 계층에서 Key 검증
  -> 내부 사용자 확인
  -> EndUserSecurityContext Payload 생성
  -> SQL 실행 전에 DB Connection에 Security Context Attach
  -> DATA ROLE / DATA GRANT 적용
```

반대로 이 변환을 하지 않고 ORDS가 공통 DB 계정으로만 접속하면, DDS는 Bearer Key별 사용자를 구분하지 못한다. 이 경우 `DB user=CB_ORDS`, `key user=cb_dds_hr` 같은 매핑값은 호출 애플리케이션 내부 변수일 뿐이고 DDS 보안 Identity는 아니다.

지원 드라이버 또는 호출 애플리케이션 계층 기준 의사 코드:

```python
import oracledb

# 1. 호출 애플리케이션에서 Bearer API Key 검증 후 내부 사용자 식별
end_user_name = "cb_dds_hr"
lookup_key = "<security-context-lookup-key-for-this-user>"
database_access_token = "<db-access-token-issued-by-iam>"

# 2. DDS End-user Security Context Payload 생성
user_context = oracledb.create_end_user_security_context(
    # Local End-user 방식: (end user name, security context lookup key)
    end_user_identity=(end_user_name, lookup_key),
    database_access_token=database_access_token,
    attributes={
        "hr.hcm_context": {
            "emp_no": "E10234",
            "dept_code": "HR"
        }
    }
)

# 3. DB 연결에 Attach 후 조회
connection.set_end_user_security_context(user_context)
try:
    cursor = connection.cursor()
    cursor.execute("""
        SELECT doc_id, title
        FROM   admin.cb_dds_v_search_documents
    """)
finally:
    connection.clear_end_user_security_context()
```

주의:

| 항목 | 의미 |
|---|---|
| Local End User의 DATA ROLE | 호출 애플리케이션이 임의로 넣는 것이 아니라 DB에 Grant된 DATA ROLE이 적용됨 |
| `database_access_token` | 호출 애플리케이션이 DB에 Security Context를 Attach할 권한이 있음을 증명하는 Token |
| 현재 ADB 로컬 실행 예제 | DDS Direct `END USER` Logon은 실행 검증됨. ORDS Handler의 DDS Bearer 단독 매핑 경로는 차단됨. Driver 기반 Security Context Attach는 IAM/DB Access Token 구성이 필요하므로 별도 구성 필요 |

검증 근거:

| 문서 | 확인한 내용 |
|---|---|
| [End-User Security Context](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/end-user-security-context.html) | Driver가 End-user Security Context Payload를 Connection Metadata로 전달하고, DB가 이를 검증해 Context를 생성/Attach |
| [Configure the Database for Local End-User Authentication](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/configure-database-local-end-user-authentication.html) | Local End User는 호출 애플리케이션이 제공한 User Name과 Security Context Lookup Key로 식별 |
| [Configure Python Applications](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/configure-python-applications.html) | `create_end_user_security_context`, `set_end_user_security_context`, `clear_end_user_security_context` API 지원 |
| [Read End-User Context Attributes](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/read-end-user-context-attributes.html) | `ORA_END_USER_CONTEXT`로 현재 Security Context의 Attribute 참조 가능 |
| [ORDS parameter binding](https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/25.3/orddg/ORDS-reference.html) | `ORDS.DEFINE_PARAMETER`로 HTTP 헤더를 Handler Bind Variable에 매핑 가능 |

</details>

## 3. 권한 매핑 테이블 설계

이 장에서는 VPD 경로에서 사용할 업무 권한 테이블을 정의한다. 핵심 구조는 `사용자 -> 역할 -> 보호 객체 권한 -> 행 규칙`이며, Bearer Key는 내부 사용자로 매핑되는 식별 수단으로 둔다.

<details>
<summary><strong>상세 설명</strong> - Key User, Role, Permission, 행 규칙</summary>

VPD에서 권한은 사용자, 역할, 보호 객체, 행(row) 조건으로 구분한다.

```mermaid
%%{init: {'themeVariables': {'fontSize': '17px'}}}%%
erDiagram
  APP_USER ||--o{ AGENT_BEARER_KEY : "key maps to user"
  APP_USER ||--o{ USER_ROLE : "user has role"
  APP_ROLE ||--o{ USER_ROLE : "assigned to user"
  APP_ROLE ||--o{ PERMISSION : "role grants permission"
  PERMISSION ||--o{ PERMISSION_RULE : "permission has row rule"

  APP_USER {
    NUMBER user_id PK
    VARCHAR2 db_username
    VARCHAR2 employee_no
    VARCHAR2 dept_code
    CHAR can_read_contents
    CHAR active
  }

  AGENT_BEARER_KEY {
    NUMBER key_id PK
    NUMBER user_id FK
    VARCHAR2 key_hash
    DATE expires_at
    CHAR active
  }

  APP_ROLE {
    NUMBER role_id PK
    VARCHAR2 role_name
  }

  USER_ROLE {
    NUMBER user_id FK
    NUMBER role_id FK
  }

  PERMISSION {
    NUMBER perm_id PK
    NUMBER role_id FK
    VARCHAR2 target_name
    VARCHAR2 action_name
  }

  PERMISSION_RULE {
    NUMBER rule_id PK
    NUMBER perm_id FK
    VARCHAR2 rule_type
    VARCHAR2 rule_value
  }
```

테이블 의미:

| 테이블 | 의미 |
|---|---|
| `APP_USER` | 내부 사용자. DB 계정 또는 Bearer Key가 최종적으로 가리키는 대상 |
| `AGENT_BEARER_KEY` | Bearer Key로 내부 사용자를 찾기 위한 테이블 |
| `APP_ROLE` | 권한 묶음 |
| `USER_ROLE` | 사용자와 역할 매핑 |
| `PERMISSION` | 어떤 보호 객체(VIEW/TABLE)를 어떤 작업으로 볼 수 있는지 |
| `PERMISSION_RULE` | 해당 보호 객체 안에서 어떤 행(row)을 볼 수 있는지 |

`PERMISSION.target_name`에는 `V_SEARCH_DOCUMENTS`, `T_SEARCH_DOCUMENTS`, `V_EMPLOYEE_DOCS`처럼 보호 객체 이름을 등록한다. VPD 함수는 하드코딩된 객체 이름이 아니라 Oracle이 넘겨준 `p_object` 값으로 이 컬럼을 조회한다.

</details>

## 4. VPD 기반 접근 통제 설계

이 장에서는 ORDS가 Bearer Key를 내부 사용자로 매핑한 뒤, DB Session Context와 VPD 정책 함수로 행(row)을 제한하는 방식을 설명한다. 컬럼 값 NULL/마스킹은 VPD가 아니라 Oracle Data Redaction으로 분리해 설명한다.

<details>
<summary><strong>상세 설명</strong> - SYS_CONTEXT, p_object, EXISTS, Redaction</summary>

VPD는 보호 객체(VIEW/TABLE)에 정책 함수를 연결한다. Agent나 ORDS가 권한 조건을 직접 붙이지 않아도, DB가 조회 시점에 조건을 자동으로 붙인다.

![VPD 조회 조건 적용 흐름](assets/agent-ords-security-vpd-where-flow.svg)

VPD를 이해할 때 필요한 세 가지:

| 요소 | 역할 |
|---|---|
| `SYS_CONTEXT` | 현재 요청자 값 확인. 예: `USER_ID`, 사번, 부서코드 |
| `p_object` | 현재 조회 중인 보호 객체 이름 |
| `EXISTS` | 권한 테이블에 접근 근거가 있는지 확인 |

ORDS Handler가 실행하는 SQL은 검색 조건만 갖는다.

```sql
SELECT doc_id, title, owner_emp_no, dept_code
FROM   app.v_search_documents
WHERE  contains_text = :q;
```

권한 판단은 VPD 함수가 수행한다.

```sql
EXISTS (
  SELECT 1
  FROM   app.user_role ur
  JOIN   app.permission p
  ON     p.role_id = ur.role_id
  JOIN   app.permission_rule r
  ON     r.perm_id = p.perm_id
  WHERE  ur.user_id = TO_NUMBER(SYS_CONTEXT('AGENT_CTX', 'USER_ID'))
  AND    p.target_name = '<p_object 값>'
  AND    p.action_name = 'SELECT'
  AND   (
          r.rule_type = 'ALL'
          OR (r.rule_type = 'MY_DEPT'
              AND dept_code = SYS_CONTEXT('AGENT_CTX', 'DEPT_CODE'))
          OR (r.rule_type = 'SELF'
              AND owner_emp_no = SYS_CONTEXT('AGENT_CTX', 'EMP_NO'))
        )
)
```

의미:

| 판단 | 확인 방식 | 결과 |
|---|---|---|
| 이 보호 객체를 조회할 수 있는가 | `permission.target_name = p_object` | 없으면 0건 |
| 이 행(row)을 볼 수 있는가 | `permission_rule`과 행 컬럼 비교 | 조건에 맞는 행(row)만 반환 |

즉, ORDS runtime schema에 보호 VIEW/TABLE 조회 권한이 있더라도 업무 권한 매핑이 없으면 결과는 나오지 않는다.

```text
DB SELECT Grant 있음
+ VPD policy 연결됨
+ user_role / permission / permission_rule 매핑 없음
= 조회 가능하지만 결과 0건
```

반대로 보호 객체 자체에 대한 DB 권한이 없거나 원본 TABLE을 직접 조회하려고 하면 VPD 판단 전 단계에서 객체가 보이지 않거나 권한 오류가 발생한다.

```text
보호 VIEW/TABLE DB 권한 미부여
= ORA-00942 또는 ORA-01031
```

### 4.1 SYS_CONTEXT 동작

![SYS_CONTEXT 동작 방식](assets/agent-ords-security-sys-context-flow.svg)

ORDS Handler는 요청자를 식별한 뒤 같은 DB 세션에 값을 저장한다.

```sql
DBMS_SESSION.SET_CONTEXT('AGENT_CTX', 'USER_ID', '42');
DBMS_SESSION.SET_CONTEXT('AGENT_CTX', 'EMP_NO', 'E10234');
DBMS_SESSION.SET_CONTEXT('AGENT_CTX', 'DEPT_CODE', 'HR');
```

VPD 정책 함수는 조회 시점에 같은 세션에서 값을 읽는다.

```sql
SYS_CONTEXT('AGENT_CTX', 'USER_ID')    -- 42
SYS_CONTEXT('AGENT_CTX', 'EMP_NO')     -- E10234
SYS_CONTEXT('AGENT_CTX', 'DEPT_CODE')  -- HR
```

조회 대상 이름은 `SYS_CONTEXT`에 넣지 않는다. Oracle이 VPD 정책 함수를 호출할 때 `p_object` 인자로 현재 조회 중인 보호 객체 이름을 넘겨준다.

### 4.2 여러 VIEW/TABLE 조회

여러 보호 객체가 한 SQL에 들어가도 원리는 같다. VPD는 정책이 연결된 각 VIEW/TABLE마다 따로 적용된다.

```sql
SELECT d.doc_id,
       d.title,
       e.employee_name
FROM   app.v_search_documents d
JOIN   app.v_employee_docs e
ON     e.owner_emp_no = d.owner_emp_no
WHERE  d.contains_text = :q;
```

의미를 풀면 아래와 같다.

```sql
SELECT d.doc_id,
       d.title,
       e.employee_name
FROM   app.v_search_documents d
JOIN   app.v_employee_docs e
ON     e.owner_emp_no = d.owner_emp_no
WHERE  d.contains_text = :q
AND    EXISTS (V_SEARCH_DOCUMENTS 권한 확인)
AND    EXISTS (V_EMPLOYEE_DOCS 권한 확인);
```

원본 TABLE을 직접 조회할 수 있게 열어두면 보호 객체를 우회할 수 있다. 따라서 API가 조회할 보호 객체만 권한을 주고, 원본 TABLE과 권한 테이블은 직접 조회 권한을 주지 않는다.

### 4.3 컬럼 처리

VPD 기본 정책은 행(row) 제한을 담당한다. 컬럼을 허용하거나 제외하는 권한 모델로는 사용하지 않는다. 이 문서의 실행 예제에서 `contents` 컬럼을 NULL로 만드는 것은 VPD가 아니라 **Oracle Data Redaction**이다.

정확히 나누면 아래와 같다.

| 구분 | 역할 | 이 문서의 적용 |
|---|---|---|
| VPD 기본 정책 | `WHERE`에 붙을 행(row) 조건 반환 | 사용 |
| Redaction | 특정 컬럼 값을 NULL 또는 마스킹 값으로 변환 | 사용. 컬럼 Grant가 아니라 값 마스킹 |
| VIEW/TABLE 설계 | 애초에 노출 객체에서 컬럼을 제외 | 선택 가능 |
| DDS DATA GRANT | `AS SELECT (ALL COLUMNS EXCEPT ...)`로 컬럼 허용/제외 | DDS 경로에서 사용 |
| Column-level VPD 옵션 | `sec_relevant_cols`, `sec_relevant_cols_opt` 옵션이 있으나 이 구조의 권한 모델로는 사용하지 않음 | 미사용 |

따라서 아래 예제에서 컬럼 값 NULL 처리는 `DBMS_REDACT.ADD_POLICY`가 수행한다. VPD는 어떤 행(row)이 반환될지를 결정하고, Redaction은 반환되는 행(row) 안의 `contents` 값을 보일지 NULL로 바꿀지를 결정한다.

컬럼 허용/제외를 권한으로 선언해야 한다면 DDS의 `DATA GRANT` 또는 별도 VIEW/TABLE 설계를 사용한다.

| 방식 | 의미 | 적합한 경우 |
|---|---|---|
| 보호 VIEW/TABLE 설계 | API에서 노출할 객체에 필요한 컬럼만 포함 | 특정 API에서 컬럼을 아예 제공하지 않을 때 |
| Oracle Data Redaction | 컬럼 값을 NULL, 부분 마스킹, 정규식 마스킹 값으로 변환 | 같은 객체를 쓰되 사용자 권한에 따라 컬럼 값을 숨길 때 |
| DDS DATA GRANT | 컬럼 허용/제외를 선언 | DDS END USER 또는 Security Context 기반 통제가 가능할 때 |
| 별도 VIEW/TABLE 분리 | 민감 컬럼이 있는 객체와 없는 객체를 분리 | 컬럼 권한이 단순하고 운영 분리가 쉬울 때 |

예:

```sql
BEGIN
  DBMS_REDACT.ADD_POLICY(
    object_schema => USER,
    object_name   => 'CB_V_SEARCH_DOCUMENTS',
    column_name   => 'CONTENTS',
    policy_name   => 'CB_CONTENTS_REDACT',
    function_type => DBMS_REDACT.NULLIFY,
    expression    =>
      'SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') IS NULL
       OR SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') != ''Y'''
  );
END;
/
```

| 상태 | 행(row) 제한 | `contents` 컬럼 |
|---|---|---|
| HR Key | HR 행(row)만 조회 | NULL |
| FIN Self Key | 본인 행(row)만 조회 | NULL |
| ALL Key | 전체 행(row) 조회 | 원문 표시 |

### 4.4 VPD 정책 연결

`DBMS_RLS.ADD_POLICY`는 권한을 직접 부여하는 코드가 아니다. 보호 객체를 조회할 때 어떤 VPD 함수를 실행할지 연결하는 코드다.

```sql
BEGIN
  DBMS_RLS.ADD_POLICY(
    object_schema   => 'APP',
    object_name     => 'V_SEARCH_DOCUMENTS',
    policy_name     => 'P_AGENT_DOC_FILTER',
    function_schema => 'APP',
    policy_function => 'AGENT_DOC_VPD_FILTER',
    statement_types => 'SELECT',
    enable          => TRUE
  );
END;
/
```

파라미터 매핑:

| `ADD_POLICY` 항목 | 예제 값 | 역할 | 연결되는 값 |
|---|---|---|---|
| `object_schema` | `APP` | VPD를 적용할 보호 객체의 소유 schema | VPD 함수의 `p_schema` |
| `object_name` | `V_SEARCH_DOCUMENTS` | VPD를 적용할 보호 객체 이름 | VPD 함수의 `p_object`, `permission.target_name` |
| `policy_name` | `P_AGENT_DOC_FILTER` | VPD 정책 관리 이름 | 정책 조회, 비활성화, 삭제 |
| `function_schema` | `APP` | VPD 정책 함수가 있는 schema | `APP.AGENT_DOC_VPD_FILTER` 호출 |
| `policy_function` | `AGENT_DOC_VPD_FILTER` | 행(row) 필터 조건을 반환하는 함수 | `EXISTS (...)` 반환 |
| `statement_types` | `SELECT` | 적용할 SQL 작업 | 조회 SQL에만 적용 |
| `enable` | `TRUE` | 정책 활성화 여부 | 등록 즉시 적용 |

실행 시 흐름:

1. `APP.V_SEARCH_DOCUMENTS`에 연결된 `P_AGENT_DOC_FILTER` 정책 확인
2. `APP.AGENT_DOC_VPD_FILTER` 함수 호출
3. `p_schema = 'APP'`, `p_object = 'V_SEARCH_DOCUMENTS'` 전달
4. 함수가 `SYS_CONTEXT`와 권한 테이블 확인
5. 함수가 반환한 조건을 원래 SQL에 추가
6. 조건을 통과한 행(row)만 반환

적용 단위:

| 검토 항목 | 기준 |
|---|---|
| `ADD_POLICY`에서 모든 객체를 한 번에 지정할 수 있는가 | 아니다. `object_schema`, `object_name`으로 보호 객체 하나를 지정한다. |
| `object_name => '*'` 또는 `%` 같은 Wildcard를 쓸 수 있는가 | 아니다. VPD `ADD_POLICY`의 `object_name`은 실제 TABLE, VIEW, SYNONYM 이름이다. |
| `statement_types`에 여러 SQL 작업을 지정하면 전체 객체 적용인가 | 아니다. `SELECT, INSERT, UPDATE, DELETE`처럼 적용할 SQL 작업 종류를 지정하는 값이다. 보호 객체 Wildcard가 아니다. |
| 같은 정책 함수를 여러 객체에 재사용할 수 있는가 | 가능하다. 같은 함수가 `p_object`를 받아 현재 조회 대상별 권한을 판단하게 만들 수 있다. |
| 객체가 많으면 어떻게 등록하는가 | 보호 객체 목록을 기준으로 `DBMS_RLS.ADD_POLICY`를 반복 실행하는 등록 스크립트를 만든다. |
| 정책이 붙지 않은 객체는 어떻게 되는가 | 그 객체에는 VPD가 적용되지 않는다. 그래서 원본 TABLE 직접 권한을 주지 않고, 노출 VIEW/TABLE 목록을 관리해야 한다. |

여러 보호 객체에 같은 VPD 함수를 붙이는 등록 스크립트 예:

```sql
BEGIN
  FOR r IN (
    SELECT 'V_SEARCH_DOCUMENTS' AS object_name FROM dual
    UNION ALL
    SELECT 'V_EMPLOYEE_DOCS' AS object_name FROM dual
  ) LOOP
    DBMS_RLS.ADD_POLICY(
      object_schema   => 'APP',
      object_name     => r.object_name,
      policy_name     => 'P_AGENT_' || r.object_name,
      function_schema => 'APP',
      policy_function => 'AGENT_DOC_VPD_FILTER',
      statement_types => 'SELECT',
      enable          => TRUE
    );
  END LOOP;
END;
/
```

이 방식에서 권한 함수는 `p_object`로 현재 보호 객체 이름을 받고, `permission.target_name`과 비교한다. 그래서 정책 함수는 하나로 유지하고, 정책 연결만 보호 객체별로 반복할 수 있다.

보호 객체 목록 테이블을 쓰는 예:

```sql
CREATE TABLE app_protected_object (
  object_name VARCHAR2(128) PRIMARY KEY,
  enabled     CHAR(1) DEFAULT 'Y' CHECK (enabled IN ('Y','N')) NOT NULL
);

INSERT INTO app_protected_object(object_name) VALUES ('V_SEARCH_DOCUMENTS');
INSERT INTO app_protected_object(object_name) VALUES ('V_EMPLOYEE_DOCS');

BEGIN
  FOR r IN (
    SELECT object_name
    FROM   app_protected_object
    WHERE  enabled = 'Y'
  ) LOOP
    DBMS_RLS.ADD_POLICY(
      object_schema   => 'APP',
      object_name     => r.object_name,
      policy_name     => 'P_AGENT_' || r.object_name,
      function_schema => 'APP',
      policy_function => 'AGENT_DOC_VPD_FILTER',
      statement_types => 'SELECT',
      enable          => TRUE
    );
  END LOOP;
END;
/
```

정리하면 “전체 적용”은 한 번의 Wildcard 지정이 아니라 “보호 객체 목록 전체를 돌면서 객체별 `ADD_POLICY`를 생성”하는 방식이다.

정책 연결 확인:

```sql
SELECT object_owner,
       object_name,
       policy_name,
       function,
       sel,
       enable
FROM   dba_policies
WHERE  object_owner = 'APP'
ORDER  BY object_name, policy_name;
```

</details>

## 5. DDS 기반 접근 통제 설계

이 장에서는 DDS `END USER`, `DATA ROLE`, `DATA GRANT`를 이용해 보호 객체의 행(row), 컬럼(column), 작업(action)을 선언형으로 통제하는 방식을 설명한다. Bearer Key를 DDS에 적용하려면 Key 조회 결과가 `EndUserSecurityContext`로 DB 호출 전에 전파되어야 한다.

<details>
<summary><strong>상세 설명</strong> - END USER, DATA ROLE, DATA GRANT</summary>

DDS는 Oracle 보안 오브젝트로 권한을 선언한다. VPD처럼 권한 테이블을 읽는 함수가 아니라, `END USER -> DATA ROLE -> DATA GRANT -> 보호 객체` 구조다.

![DDS 보안 오브젝트 모델](assets/agent-ords-security-dds-object-model.svg)

![DDS 설정 흐름](assets/agent-ords-security-dds-setup-flow.svg)

`DATA GRANT`는 어떤 보호 객체(VIEW/TABLE)에 대해 어떤 행(row), 컬럼(column), 작업(operation)을 허용하는지 선언한다.

| 항목 | DDS 표현 |
|---|---|
| 사용자 | `CREATE END USER` |
| 역할 | `CREATE DATA ROLE` |
| 대상 | `DATA GRANT ... ON <VIEW 또는 TABLE>` |
| 행(row) 제한 | `WHERE dept_code = 'HR'` |
| 컬럼(column) 허용/제외 | `AS SELECT (ALL COLUMNS EXCEPT contents)` |
| 권한 미부여 | `ORA-00942`, 객체 자체가 보이지 않음 |

참고로 DDS에는 `CREATE APPLICATION IDENTITY`도 있다. 이 객체는 애플리케이션 자체를 DB 보안 Identity로 등록하고 DATA ROLE을 부여할 때 사용한다. 본 문서의 실행 예제는 사용자별 권한 차이를 보여주기 위해 `END USER`만 사용한다.

DDS에서 Bearer Key를 사용하려면 Key 자체를 `DATA GRANT`가 읽는 것이 아니라, Key 검증 결과가 DDS `EndUserSecurityContext`로 DB 호출 전에 전달되어야 한다. ORDS PL/SQL Handler 안에서 Key를 테이블 조회로 매핑하는 것만으로는 DDS 보안 사용자가 바뀌지 않는다. 이 패턴은 아래 ADB 검증에서 `EXPECTED_BLOCKED`로 확인했다.

기본 예:

```sql
CREATE END USER "agent_hr_001"
  IDENTIFIED BY "&AGENT_HR_001_PASSWORD";

CREATE ROLE dds_connect_role;
GRANT CREATE SESSION TO dds_connect_role;

CREATE DATA ROLE hr_search_role;
GRANT dds_connect_role TO hr_search_role;
GRANT DATA ROLE hr_search_role TO "agent_hr_001";

CREATE OR REPLACE DATA GRANT admin.dg_hr_search_documents
  AS SELECT
  ON admin.v_search_documents
  WHERE dept_code = 'HR'
  TO hr_search_role;
```

행(row) 제한과 컬럼(column) 허용/제외를 한 번에 선언하는 예:

```sql
CREATE OR REPLACE DATA GRANT admin.dg_hr_search_documents
  AS SELECT (ALL COLUMNS EXCEPT contents)
  ON admin.v_search_documents
  WHERE dept_code = 'HR'
  TO hr_search_role;
```

여러 보호 객체를 함께 조회하면 각 대상에 대한 `DATA GRANT`가 필요하다.

```sql
CREATE OR REPLACE DATA GRANT admin.dg_hr_search_documents
  AS SELECT
  ON admin.v_search_documents
  WHERE dept_code = 'HR'
  TO hr_search_role;

CREATE OR REPLACE DATA GRANT admin.dg_hr_employee_docs
  AS SELECT
  ON admin.v_employee_docs
  WHERE dept_code = 'HR'
  TO hr_search_role;
```

`v_search_documents` 권한만 있고 `v_employee_docs` 권한이 없으면, 두 보호 객체를 함께 조회하는 SQL은 `v_employee_docs` 접근 단계에서 실패하거나 허용되지 않는다.

DDS 적용과 확인 단위:

| 검토 항목 | 기준 |
|---|---|
| DDS도 객체별로 스크립트가 필요한가 | `DATA GRANT`는 보호 객체, DATA ROLE, 조건 단위로 선언한다. 객체/역할/조건이 다르면 별도 `DATA GRANT`가 필요하다. |
| 권한을 한눈에 확인할 수 있는가 | 가능하다. `DBA_DATA_GRANTS`와 `DBA_DATA_ROLE_GRANTS`를 조합하면 DATA ROLE, 보호 객체, 행 조건, 제외 컬럼을 볼 수 있다. |
| 한눈에 볼 수 없는 것은 무엇인가 | 승인 요청 사유, 업무 결재 상태, Ticket 번호 같은 업무 메타데이터는 DDS Dictionary에 없다. 별도 승인 테이블이나 Ticket 시스템에 남겨야 한다. |
| 운영에서 권장하는 방식 | `DATA GRANT` 이름 규칙과 보호 객체 목록을 표준화하고, Dictionary 조회 결과를 정기 점검 또는 배포 검증에 포함한다. |

DDS 권한 요약 조회:

```sql
SELECT grant_name,
       object_name,
       grantee,
       privilege,
       COALESCE(
         LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_name),
         'ALL COLUMNS'
       ) AS allowed_columns,
       COALESCE(MAX(granted_with_all_columns_except), '-') AS excluded_columns,
       predicate
FROM   dba_data_grants
WHERE  owner = 'ADMIN'
GROUP  BY grant_name, object_name, grantee, privilege, predicate
ORDER  BY object_name, grant_name;
```

DDS end user별 권한 매트릭스 조회:

```sql
SELECT rg.grantee AS end_user_name,
       rg.data_role,
       dg.grant_name,
       dg.object_name,
       dg.predicate,
       COALESCE(MAX(dg.granted_with_all_columns_except), '-') AS excluded_columns
FROM   dba_data_role_grants rg
LEFT   JOIN dba_data_grants dg
ON     dg.grantee = rg.data_role
GROUP  BY rg.grantee,
          rg.data_role,
          dg.grant_name,
          dg.object_name,
          dg.predicate
ORDER  BY rg.grantee, dg.object_name, dg.grant_name;
```

</details>

## 6. VPD/DDS 비교 및 적용 기준

이 장에서는 앞에서 분리 설명한 VPD와 DDS를 운영 기준으로 비교한다. 비교 기준은 사용자 식별 방식, 권한 저장 위치, 행/컬럼 통제 범위, Bearer Key 처리 방식, 적용 단위, 권한 미부여 시 결과로 둔다.

<details>
<summary><strong>상세 설명</strong> - 운영 선택 기준</summary>

| 항목 | VPD | DDS |
|---|---|---|
| 권한 표현 방식 | PL/SQL 정책 함수가 SQL 조건 반환 | `CREATE DATA GRANT`로 권한 선언 |
| 정책 적용 단위 | `DBMS_RLS.ADD_POLICY` 1회당 보호 객체 1개 | `DATA GRANT` 1개당 보호 객체, 역할, 조건 1묶음 |
| 권한 저장 위치 | 업무 권한 테이블 | Oracle Dictionary의 DDS 보안 오브젝트 |
| 중앙 확인 방법 | `DBA_POLICIES`, `REDACTION_POLICIES`, 업무 권한 테이블 | `DBA_DATA_GRANTS`, `DBA_DATA_ROLE_GRANTS`, `DBA_DATA_ROLES`, `DBA_END_USERS` |
| 사용자 식별 | DB 계정, Bearer Key, `SYS_CONTEXT` 조합 | `END USER`, `DATA ROLE` |
| DB user와 실제 사용자 | 공통 DB 계정 아래 다수 key 사용자를 테이블로 구분 가능 | DDS 사용자가 실제 사용자에 가깝게 매핑되어야 명확 |
| Bearer Key 처리 | Key Hash를 테이블에서 찾아 Context 저장 | Key User를 DDS `EndUserSecurityContext`로 전파해야 가능. ORDS Handler의 DDS Bearer 단독 매핑은 차단됨 |
| 보호 객체 | VIEW 또는 TABLE | VIEW 또는 TABLE |
| 행(row) 제한 | `EXISTS`, `dept_code`, `owner_emp_no` 조건 반환 | `DATA GRANT ... WHERE ...` |
| 컬럼(column) 허용/제외 | VIEW/TABLE 설계. Redaction은 값 마스킹/NULL | `AS SELECT (ALL COLUMNS EXCEPT ...)` |
| 권한 미부여 시 | 0건 반환 가능 | 객체 자체가 보이지 않음. 예: `ORA-00942` |
| 권한 변경 | 권한 테이블 `INSERT/UPDATE/DELETE` | `CREATE OR REPLACE DATA GRANT`, `DROP DATA GRANT`, `GRANT DATA ROLE` |
| 운영상 주의 | 정책을 붙이지 않은 객체는 보호되지 않음 | 승인 사유 같은 업무 메타데이터는 Dictionary에 없음 |
| 적합한 경우 | key 사용자 많음, 권한 변경 잦음, 테이블 기반 운영 | 역할이 안정적이고 정책을 DDL로 명확히 관리 |

선택 기준:

| 선택지 | 추천 상황 | 설명 |
|---|---|---|
| VPD | Bearer Key 사용자가 많고 세분화된 업무 권한 테이블이 필요한 경우 | `DB user != key user` 구조에 적합 |
| DDS | DDS `END USER` Identity를 명확히 전파할 수 있는 경우 | 선언형 보안 오브젝트 관리에 적합 |
| 둘 다 비교 | 전환기 또는 기능 검증 | 같은 VIEW/TABLE에 동시에 걸지 말고 별도 보호 객체로 분리 |

</details>

## 7. ADB 실행 검증

이 장에서는 ADB에 생성한 로컬 테이블 기준으로 VPD + Redaction, DDS, ORDS Handler Bearer 검증을 실행한 스크립트와 결과를 제시한다.

<details>
<summary><strong>상세 설명</strong> - 실행 가능한 VPD/DDS 스크립트와 결과</summary>

이 예제는 RDS, DB Link, Postgres, MySQL을 사용하지 않는다. ADB 안에 로컬 테이블을 만들고, 같은 흐름에서 VPD 경로와 DDS 경로를 각각 확인한다.

실행:

```bash
./scripts/run_agent_ords_security_adb_local.sh
```

실행 파일:

| 파일 | 역할 |
|---|---|
| `scripts/run_agent_ords_security_adb_local.sh` | 전체 실행 |
| `sql/adb/16_agent_ords_security_local_cleanup.sql` | `CB_*` 예제 객체 정리 |
| `sql/adb/17_agent_ords_security_local_vpd_setup.sql` | VPD용 로컬 테이블, 권한 테이블, context, redaction, 정책 생성 |
| `sql/adb/18_agent_ords_security_local_vpd_test.sql` | `CB_ORDS`로 Bearer Key 기반 VPD 테스트 |
| `sql/adb/19_agent_ords_security_local_dds_setup.sql` | DDS용 로컬 테이블, END USER, DATA ROLE, DATA GRANT 생성 |
| `sql/adb/20_agent_ords_security_local_dds_test.sql` | DDS end user별 조회 테스트 |
| `sql/adb/21_agent_ords_security_ords_enable_schema.sql` | `CB_ORDS` schema를 ORDS에 enable |
| `sql/adb/22_agent_ords_security_ords_handler_setup.sql` | ORDS Module/Handler와 Handler Package 생성 |
| `sql/adb/23_agent_ords_security_ords_handler_test.sql` | Handler Package 직접 실행으로 VPD/DDS Bearer 경로 검증 |
| `sql/adb/24_agent_ords_security_inventory.sql` | VPD/DDS 정책과 권한을 중앙 조회 |

### 7.1 VPD + Redaction 실행 검증

VPD 예제의 흐름:

1. ORDS runtime schema `CB_ORDS`가 DB에 접속
2. ORDS Handler가 Bearer Key를 내부 사용자로 매핑
3. 내부 사용자 값을 `CB_AGENT_CTX`에 저장
4. `CB_V_SEARCH_DOCUMENTS` 조회
5. VPD가 행(row) 제한
6. Redaction이 `contents` 값을 NULL/마스킹 처리

사용한 핵심 스크립트:

```sql
CREATE TABLE cb_search_documents (
  doc_id        NUMBER PRIMARY KEY,
  title         VARCHAR2(100) NOT NULL,
  owner_emp_no  VARCHAR2(20)  NOT NULL,
  dept_code     VARCHAR2(20)  NOT NULL,
  contents      VARCHAR2(4000),
  created_at    DATE DEFAULT SYSDATE NOT NULL
);

CREATE OR REPLACE VIEW cb_v_search_documents AS
SELECT doc_id, title, owner_emp_no, dept_code, contents, created_at
FROM   cb_search_documents;

CREATE TABLE cb_app_user (
  user_id            NUMBER PRIMARY KEY,
  user_name          VARCHAR2(50) NOT NULL,
  employee_no        VARCHAR2(20) NOT NULL,
  dept_code          VARCHAR2(20) NOT NULL,
  can_read_contents  CHAR(1) DEFAULT 'N' CHECK (can_read_contents IN ('Y','N')) NOT NULL,
  active             CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N')) NOT NULL
);

INSERT INTO cb_app_user VALUES (101, 'agent_hr',       'E10234', 'HR',  'N', 'Y');
INSERT INTO cb_app_user VALUES (102, 'agent_fin_self', 'E2001',  'FIN', 'N', 'Y');
INSERT INTO cb_app_user VALUES (103, 'agent_all',      'E99999', 'HQ',  'Y', 'Y');

INSERT INTO cb_agent_bearer_key(key_id, user_id, key_hash, key_prefix)
VALUES (1, 101, STANDARD_HASH('cb_hr_key', 'SHA256'), 'cb_hr');
```

Bearer Key 매핑:

```sql
CREATE OR REPLACE CONTEXT cb_agent_ctx USING cb_agent_ctx_pkg;

CREATE OR REPLACE PACKAGE BODY cb_agent_ctx_pkg AS
  PROCEDURE set_user_by_bearer(p_bearer_key IN VARCHAR2) AS
    v_user_id           cb_app_user.user_id%TYPE;
    v_emp_no            cb_app_user.employee_no%TYPE;
    v_dept_code         cb_app_user.dept_code%TYPE;
    v_can_read_contents cb_app_user.can_read_contents%TYPE;
  BEGIN
    SELECT u.user_id, u.employee_no, u.dept_code, u.can_read_contents
    INTO   v_user_id, v_emp_no, v_dept_code, v_can_read_contents
    FROM   cb_agent_bearer_key k
    JOIN   cb_app_user u
    ON     u.user_id = k.user_id
    WHERE  k.key_hash = STANDARD_HASH(p_bearer_key, 'SHA256')
    AND    k.active = 'Y'
    AND    k.revoked_at IS NULL
    AND    (k.expires_at IS NULL OR k.expires_at > SYSDATE)
    AND    u.active = 'Y';

    set_user_values(v_user_id, v_emp_no, v_dept_code, v_can_read_contents);
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      clear_user;
      RAISE_APPLICATION_ERROR(-20002, 'Invalid or expired Bearer key');
  END;
END;
/
```

VPD와 Redaction:

```sql
CREATE OR REPLACE FUNCTION cb_agent_doc_vpd_filter(
  p_schema IN VARCHAR2,
  p_object IN VARCHAR2
) RETURN VARCHAR2
AUTHID DEFINER
AS
  v_user_id     VARCHAR2(30);
  v_target_name VARCHAR2(128);
BEGIN
  v_user_id := SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID');

  IF v_user_id IS NULL THEN
    RETURN '1 = 0';
  END IF;

  v_target_name := REPLACE(UPPER(p_object), '''', '''''');

  RETURN
    'EXISTS (
       SELECT 1
       FROM   admin.cb_user_role ur
       JOIN   admin.cb_permission p ON p.role_id = ur.role_id
       JOIN   admin.cb_permission_rule r ON r.perm_id = p.perm_id
       WHERE  ur.user_id = TO_NUMBER(SYS_CONTEXT(''CB_AGENT_CTX'', ''USER_ID''))
       AND    p.target_name = ''' || v_target_name || '''
       AND    p.action_name = ''SELECT''
       AND   (
               r.rule_type = ''ALL''
               OR (r.rule_type = ''MY_DEPT''
                   AND dept_code = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE''))
               OR (r.rule_type = ''SELF''
                   AND owner_emp_no = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO''))
             )
     )';
END;
/

BEGIN
  DBMS_RLS.ADD_POLICY(
    object_schema   => USER,
    object_name     => 'CB_V_SEARCH_DOCUMENTS',
    policy_name     => 'CB_AGENT_DOC_POLICY',
    function_schema => USER,
    policy_function => 'CB_AGENT_DOC_VPD_FILTER',
    statement_types => 'SELECT',
    enable          => TRUE
  );
END;
/

BEGIN
  DBMS_REDACT.ADD_POLICY(
    object_schema => USER,
    object_name   => 'CB_V_SEARCH_DOCUMENTS',
    column_name   => 'CONTENTS',
    policy_name   => 'CB_CONTENTS_REDACT',
    function_type => DBMS_REDACT.NULLIFY,
    expression    =>
      'SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') IS NULL
       OR SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') != ''Y'''
  );
END;
/
```

실행 결과:

```text
=== VPD 1. No Bearer key / context: fail closed ===
ROWS_VISIBLE
------------
0

=== VPD 2. Authorization: Bearer cb_hr_key -> HR department rows ===
CTX_USER_ID  CTX_EMP_NO   CTX_DEPT_COD CTX_CAN_READ_CONTENTS
------------ ------------ ------------ ----------------------
101          E10234       HR           N

DOC_ID TITLE                    OWNER_EMP_NO DEPT_CODE CONTENTS
------ ------------------------ ------------ --------- --------
1      HR payroll guide         E1001        HR
2      HR recruiting plan       E1002        HR
6      HR benefits notice       E1003        HR

ROWS_VISIBLE
------------
3

=== VPD 3. Authorization: Bearer cb_fin_key -> self row only ===
CTX_USER_ID  CTX_EMP_NO   CTX_DEPT_COD CTX_CAN_READ_CONTENTS
------------ ------------ ------------ ----------------------
102          E2001        FIN          N

DOC_ID TITLE                    OWNER_EMP_NO DEPT_CODE CONTENTS
------ ------------------------ ------------ --------- --------
3      Finance close checklist  E2001        FIN

ROWS_VISIBLE
------------
1

=== VPD 4. Authorization: Bearer cb_all_key -> all rows ===
ROWS_VISIBLE
------------
6

DOC_ID TITLE                    OWNER_EMP_NO DEPT_CODE CONTENTS
------ ------------------------ ------------ --------- ---------------------------
1      HR payroll guide         E1001        HR        Payroll policy and HR guide
2      HR recruiting plan       E1002        HR        Recruiting plan for HR team
3      Finance close checklist  E2001        FIN       Monthly close checklist

=== VPD 5. Invalid Bearer key -> ORA-20002 and context cleared ===
ORA-20002: Invalid or expired Bearer key

ROWS_VISIBLE_AFTER_INVALID_KEY
------------------------------
0

=== VPD 6. Bypass attempts ===
SELECT COUNT(*) FROM admin.cb_search_documents
ORA-00942: table or view "ADMIN"."CB_SEARCH_DOCUMENTS" does not exist
```

VPD 결과 해석:

| 요청 상태 | 적용된 사용자 | 결과 |
|---|---|---|
| Bearer Key 없음 | 없음 | 0건 |
| `cb_hr_key` | `USER_ID=101`, `DEPT_CODE=HR`, `CAN_READ_CONTENTS=N` | HR 행(row) 3건, `contents` NULL |
| `cb_fin_key` | `USER_ID=102`, `EMP_NO=E2001`, `CAN_READ_CONTENTS=N` | 본인 행(row) 1건, `contents` NULL |
| `cb_all_key` | `USER_ID=103`, `CAN_READ_CONTENTS=Y` | 전체 6건, `contents` 원문 표시 |
| 잘못된 Key | Context 초기화 | 오류 후 0건 |

### 7.2 DDS 실행 검증

DDS 예제의 흐름:

1. DDS `END USER`로 접속
2. `DATA ROLE`로 권한 묶음 확인
3. `DATA GRANT`가 보호 객체와 행/컬럼 조건 적용
4. 권한이 없으면 객체가 보이지 않음

사용한 핵심 스크립트:

```sql
CREATE TABLE cb_dds_documents (
  doc_id        NUMBER PRIMARY KEY,
  title         VARCHAR2(100) NOT NULL,
  owner_emp_no  VARCHAR2(20)  NOT NULL,
  dept_code     VARCHAR2(20)  NOT NULL,
  contents      VARCHAR2(4000),
  created_at    DATE DEFAULT SYSDATE NOT NULL
);

CREATE OR REPLACE VIEW cb_dds_v_search_documents AS
SELECT doc_id, title, owner_emp_no, dept_code, contents, created_at
FROM   cb_dds_documents;

CREATE END USER "cb_dds_hr"   IDENTIFIED BY "CbDds#Hr2026Local1";
CREATE END USER "cb_dds_fin"  IDENTIFIED BY "CbDds#Fin2026Local1";
CREATE END USER "cb_dds_all"  IDENTIFIED BY "CbDds#All2026Local1";
CREATE END USER "cb_dds_none" IDENTIFIED BY "CbDds#None2026Local1";

CREATE ROLE cb_dds_connect_role;
GRANT CREATE SESSION TO cb_dds_connect_role;

CREATE DATA ROLE cb_dds_hr_role;
CREATE DATA ROLE cb_dds_fin_role;
CREATE DATA ROLE cb_dds_all_role;
CREATE DATA ROLE cb_dds_connect_only_role;

GRANT cb_dds_connect_role TO cb_dds_hr_role;
GRANT cb_dds_connect_role TO cb_dds_fin_role;
GRANT cb_dds_connect_role TO cb_dds_all_role;
GRANT cb_dds_connect_role TO cb_dds_connect_only_role;

GRANT DATA ROLE cb_dds_hr_role           TO "cb_dds_hr";
GRANT DATA ROLE cb_dds_fin_role          TO "cb_dds_fin";
GRANT DATA ROLE cb_dds_all_role          TO "cb_dds_all";
GRANT DATA ROLE cb_dds_connect_only_role TO "cb_dds_none";

CREATE DATA GRANT admin.cb_dg_hr_docs
  AS SELECT (ALL COLUMNS EXCEPT contents)
  ON admin.cb_dds_v_search_documents
  WHERE dept_code = 'HR'
  TO cb_dds_hr_role;

CREATE DATA GRANT admin.cb_dg_fin_docs
  AS SELECT (ALL COLUMNS EXCEPT contents)
  ON admin.cb_dds_v_search_documents
  WHERE dept_code = 'FIN'
  TO cb_dds_fin_role;

CREATE DATA GRANT admin.cb_dg_all_docs
  AS SELECT
  ON admin.cb_dds_v_search_documents
  TO cb_dds_all_role;
```

실행 결과:

```text
=== cb_dds_hr ===
END_USER_NAME
----------------
"cb_dds_hr"

ROWS_VISIBLE
------------
3

DOC_ID TITLE                OWNER_EMP_NO DEPT_CODE CONTENTS
------ -------------------- ------------ --------- --------
1      HR payroll guide     E1001        HR
2      HR recruiting plan   E1002        HR
6      HR benefits notice   E1003        HR

=== cb_dds_fin ===
ROWS_VISIBLE
------------
2

DOC_ID TITLE                    OWNER_EMP_NO DEPT_CODE CONTENTS
------ ------------------------ ------------ --------- --------
3      Finance close checklist  E2001        FIN
4      Finance audit memo       E2002        FIN

=== cb_dds_all ===
ROWS_VISIBLE
------------
6

DOC_ID TITLE                    OWNER_EMP_NO DEPT_CODE CONTENTS
------ ------------------------ ------------ --------- ---------------------------
1      HR payroll guide         E1001        HR        Payroll policy and HR guide
2      HR recruiting plan       E1002        HR        Recruiting plan for HR team
3      Finance close checklist  E2001        FIN       Monthly close checklist
4      Finance audit memo       E2002        FIN       Audit memo for finance team
5      Sales forecast           E3001        SALES     Quarterly sales forecast
6      HR benefits notice       E1003        HR        Benefits notice for employees

=== cb_dds_none ===
FROM admin.cb_dds_v_search_documents
ORA-00942: table or view "ADMIN"."CB_DDS_V_SEARCH_DOCUMENTS" does not exist
```

DDS 결과 해석:

| DDS 사용자 | DATA ROLE | DATA GRANT | 결과 |
|---|---|---|---|
| `cb_dds_hr` | `cb_dds_hr_role` | `WHERE dept_code = 'HR'`, `EXCEPT contents` | 3건, `contents` NULL |
| `cb_dds_fin` | `cb_dds_fin_role` | `WHERE dept_code = 'FIN'`, `EXCEPT contents` | 2건, `contents` NULL |
| `cb_dds_all` | `cb_dds_all_role` | 행 제한 없음, 컬럼 제외 없음 | 6건, `contents` 원문 표시 |
| `cb_dds_none` | `cb_dds_connect_only_role` | 없음 | `ORA-00942` |

### 7.3 ORDS Handler Bearer 검증

이 검증은 ORDS Handler가 `Authorization` 헤더를 받아 처리하는 구조를 ADB 안에서 재현한다. 외부 ORDS URL 호출은 하지 않고, Handler가 호출하는 같은 Package를 `CB_ORDS`로 직접 실행했다.

ORDS schema enable:

```sql
BEGIN
  ORDS.ENABLE_SCHEMA(
    p_enabled             => TRUE,
    p_schema              => 'CB_ORDS',
    p_url_mapping_type    => 'BASE_PATH',
    p_url_mapping_pattern => 'cb-ords',
    p_auto_rest_auth      => FALSE
  );
  COMMIT;
END;
/
```

Header를 Handler Bind Variable로 매핑:

```sql
ORDS.DEFINE_PARAMETER(
  p_module_name        => 'cb.agent.security',
  p_pattern            => 'vpd/documents',
  p_method             => 'POST',
  p_name               => 'Authorization',
  p_bind_variable_name => 'auth_header',
  p_source_type        => 'HEADER',
  p_param_type         => 'STRING',
  p_access_method      => 'IN'
);
```

VPD Handler 핵심:

```sql
FUNCTION vpd_search_json(p_authorization IN VARCHAR2) RETURN CLOB AS
  v_bearer_key VARCHAR2(4000);
BEGIN
  admin.cb_agent_ctx_pkg.clear_user;
  v_bearer_key := extract_bearer_key(p_authorization);
  admin.cb_agent_ctx_pkg.set_user_by_bearer(v_bearer_key);

  -- 이후 admin.cb_v_search_documents 조회.
  -- VPD는 CB_AGENT_CTX 값을 읽어 허용 행(row)만 반환한다.
END;
```

DDS Bearer 검증 핵심:

```sql
FUNCTION dds_bearer_probe_json(p_authorization IN VARCHAR2) RETURN CLOB AS
  v_bearer_key      VARCHAR2(4000);
  v_mapped_end_user VARCHAR2(128);
BEGIN
  v_bearer_key := extract_bearer_key(p_authorization);
  v_mapped_end_user := mapped_dds_end_user(v_bearer_key);

  SELECT ORA_END_USER_CONTEXT.username
  INTO   v_dds_context_username
  FROM   dual;

  EXECUTE IMMEDIATE
    'SELECT COUNT(*) FROM admin.cb_dds_v_search_documents'
  INTO v_rows_visible;
END;
```

실행 결과:

```text
=== ORDS handler probe 1. VPD path with mandatory Bearer header ===
{
  "scenario": "VPD_BEARER_HEADER",
  "db_user": "CB_ORDS",
  "context_user_id": "101",
  "context_emp_no": "E10234",
  "context_dept_code": "HR",
  "context_read_contents": "N",
  "rows": [
    {"doc_id":1,"title":"HR payroll guide","dept_code":"HR","contents":null},
    {"doc_id":2,"title":"HR recruiting plan","dept_code":"HR","contents":null},
    {"doc_id":6,"title":"HR benefits notice","dept_code":"HR","contents":null}
  ]
}

=== ORDS handler probe 2. VPD path without Bearer header ===
ORA-20101: Authorization header must be Bearer <key>

=== ORDS handler probe 3. DDS path with Bearer header only ===
{
  "scenario": "DDS_BEARER_HANDLER_PROBE",
  "db_user": "CB_ORDS",
  "bearer_mapped_end_user": "cb_dds_hr",
  "dds_context_username": null,
  "query_error": "ORA-00942: table or view \"ADMIN\".\"CB_DDS_V_SEARCH_DOCUMENTS\" does not exist",
  "result": "EXPECTED_BLOCKED"
}

=== ORDS handler probe 4. DDS path with all-access Bearer header only ===
{
  "scenario": "DDS_BEARER_HANDLER_PROBE",
  "db_user": "CB_ORDS",
  "bearer_mapped_end_user": "cb_dds_all",
  "dds_context_username": null,
  "query_error": "ORA-00942: table or view \"ADMIN\".\"CB_DDS_V_SEARCH_DOCUMENTS\" does not exist",
  "result": "EXPECTED_BLOCKED"
}
```

해석:

| 항목 | 의미 |
|---|---|
| VPD + Bearer 헤더 | ORDS Handler가 Key를 검증하고 `SYS_CONTEXT`를 채우면 정상 동작 |
| Bearer 헤더 없음 | Mandatory Header로 처리되어 차단 |
| DDS + Bearer 단독 매핑 | Key를 `cb_dds_hr`로 매핑해도 DDS Security Context가 없으므로 차단 |
| DDS 적용 조건 | 지원 드라이버 또는 호출 애플리케이션이 `EndUserSecurityContext`를 DB 호출 전에 Attach해야 함 |

### 7.4 중앙 권한 인벤토리

VPD와 DDS 모두 적용 결과를 Dictionary View로 확인할 수 있다. 이 예제에서는 `sql/adb/24_agent_ords_security_inventory.sql`을 실행해 정책 연결과 DDS Grant Matrix를 확인한다.

VPD 정책 연결 확인:

```sql
SELECT object_owner,
       object_name,
       policy_name,
       pf_owner,
       function,
       sel,
       enable
FROM   dba_policies
WHERE  object_owner = 'ADMIN'
AND    object_name LIKE 'CB%'
ORDER  BY object_name, policy_name;
```

Redaction 정책 확인:

```sql
SELECT object_owner,
       object_name,
       policy_name,
       enable,
       expression
FROM   redaction_policies
WHERE  object_owner = 'ADMIN'
AND    object_name LIKE 'CB%'
ORDER  BY object_name, policy_name;
```

DDS Grant 요약:

```sql
SELECT grant_name,
       object_name,
       grantee,
       privilege,
       COALESCE(
         LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_name),
         'ALL COLUMNS'
       ) AS allowed_columns,
       COALESCE(MAX(granted_with_all_columns_except), '-') AS excluded_columns,
       predicate
FROM   dba_data_grants
WHERE  owner = 'ADMIN'
AND    object_name LIKE 'CB%'
GROUP  BY grant_name, object_name, grantee, privilege, predicate
ORDER  BY object_name, grant_name;
```

DDS End User별 권한 매트릭스:

```sql
SELECT rg.grantee AS end_user_name,
       rg.data_role,
       dg.grant_name,
       dg.object_name,
       dg.predicate,
       COALESCE(MAX(dg.granted_with_all_columns_except), '-') AS excluded_columns
FROM   dba_data_role_grants rg
LEFT   JOIN dba_data_grants dg
ON     dg.grantee = rg.data_role
WHERE  rg.grantee LIKE 'cb\_dds\_%' ESCAPE '\'
GROUP  BY rg.grantee,
          rg.data_role,
          dg.grant_name,
          dg.object_name,
          dg.predicate
ORDER  BY rg.grantee, dg.object_name, dg.grant_name;
```

실행 결과 요약:

```text
=== Inventory 1. VPD policies attached to protected objects ===
OBJECT_OWNER OBJECT_NAME            POLICY_NAME          FUNCTION
------------ ---------------------- -------------------- ------------------------
ADMIN        CB_V_SEARCH_DOCUMENTS  CB_AGENT_DOC_POLICY  CB_AGENT_DOC_VPD_FILTER

=== Inventory 2. Redaction policies attached to protected objects ===
OBJECT_OWNER OBJECT_NAME            POLICY_NAME          ENABLE
------------ ---------------------- -------------------- ------
ADMIN        CB_V_SEARCH_DOCUMENTS  CB_CONTENTS_REDACT   YES

=== Inventory 3. DDS grant summary, one row per DATA GRANT ===
GRANT_NAME      OBJECT_NAME                GRANTEE          EXCLUDED_COLUMNS PREDICATE
--------------- -------------------------- ---------------- ---------------- ---------------
CB_DG_ALL_DOCS  CB_DDS_V_SEARCH_DOCUMENTS  CB_DDS_ALL_ROLE  -                1 = 1
CB_DG_FIN_DOCS  CB_DDS_V_SEARCH_DOCUMENTS  CB_DDS_FIN_ROLE  CONTENTS         dept_code='FIN'
CB_DG_HR_DOCS   CB_DDS_V_SEARCH_DOCUMENTS  CB_DDS_HR_ROLE   CONTENTS         dept_code='HR'

=== Inventory 4. DDS end user -> data role -> data grant matrix ===
END_USER_NAME  DATA_ROLE                 GRANT_NAME      PREDICATE
-------------- ------------------------- --------------- ---------------
cb_dds_all     CB_DDS_ALL_ROLE           CB_DG_ALL_DOCS  1 = 1
cb_dds_fin     CB_DDS_FIN_ROLE           CB_DG_FIN_DOCS  dept_code='FIN'
cb_dds_hr      CB_DDS_HR_ROLE            CB_DG_HR_DOCS   dept_code='HR'
cb_dds_none    CB_DDS_CONNECT_ONLY_ROLE  -               -
```

확인 가능한 것과 별도 관리가 필요한 것:

| 구분 | 확인 방법 |
|---|---|
| 어떤 객체에 VPD가 붙었는지 | `DBA_POLICIES` |
| 어떤 컬럼이 Redaction 대상인지 | `REDACTION_POLICIES`, `REDACTION_COLUMNS` |
| 어떤 DDS DATA GRANT가 있는지 | `DBA_DATA_GRANTS` |
| 어떤 DDS END USER가 어떤 DATA ROLE을 받았는지 | `DBA_DATA_ROLE_GRANTS` |
| 승인 요청자, 승인 일시, 사유, Ticket 번호 | DDS/VPD Dictionary만으로는 부족. 별도 승인 이력 테이블 또는 Ticket 시스템 필요 |

</details>

## 8. 결과 화면 및 운영 확인 항목

이 장에서는 고객 설명에 사용할 화면 형태의 예시를 정리한다. 사용자 식별 시나리오, 권한 매핑, Authorization 헤더 처리, VPD 결과, DDS 결과, 중앙 권한 인벤토리를 순서대로 확인한다.

<details>
<summary><strong>상세 설명</strong> - 시나리오, 권한 등록, 실행 결과 확인</summary>

### 8.1 두 사용자 식별 시나리오

![두 가지 사용자 식별 시나리오 화면](assets/agent-ords-security-two-scenarios.svg)

확인 항목:

| 항목 | 의미 |
|---|---|
| DB User 기반 | Oracle 접속 계정으로 요청자를 식별 |
| Bearer Key 기반 | ORDS Authorization 헤더의 키로 내부 사용자를 식별 |
| 공통 처리 | 식별 후 DB 보안 정책이 행/컬럼을 제한 |
| 기본 권장 흐름 | ORDS Bearer Key 기반은 VPD + Redaction을 기본으로 적용 |

### 8.2 권한 등록/매핑

![권한 등록 매핑 화면](assets/agent-ords-security-permission-mapping-screen.svg)

확인 항목:

| 항목 | 의미 |
|---|---|
| `APP_USER` | key 또는 DB user가 최종적으로 가리키는 내부 사용자 |
| `AGENT_BEARER_KEY` | Bearer Key Hash와 내부 사용자 매핑 |
| `USER_ROLE` | 사용자와 역할 연결 |
| `PERMISSION` | 어떤 보호 객체를 어떤 action으로 볼 수 있는지 |
| `PERMISSION_RULE` | 해당 보호 객체 안에서 어떤 행(row)을 볼 수 있는지 |
| `p_object` 매칭 | VPD가 현재 조회 객체 이름을 `permission.target_name`과 비교 |

### 8.3 Authorization 헤더 기반 사용자 매핑

![Authorization 헤더 기반 사용자 매핑 화면](assets/agent-ords-security-bearer-key-screen.svg)

확인 항목:

| 항목 | 의미 |
|---|---|
| ORDS Header `Bearer Key` | 필수값. 없으면 차단 |
| ORDS Handler | Key를 받아 내부 사용자로 매핑 |
| `key_hash` | DB에 저장된 비교값. 원문 Key 저장 금지 |
| 사번/부서코드 | key가 가리키는 내부 사용자 정보 |
| DB 조회 결과 | VPD/DDS 정책 적용 후 허용된 데이터만 반환 |

### 8.4 권한 미부여 및 차단 결과

![권한 미부여 및 차단 결과 화면](assets/agent-ords-security-no-permission-screen.svg)

확인 항목:

| 항목 | 의미 |
|---|---|
| 매핑 없음 | 보호 객체 DB 권한은 있어도 VPD 조건이 통과하지 못해 0건 |
| DB 권한 미부여 | VPD 판단 전 객체가 보이지 않아 `ORA-00942` 또는 `ORA-01031` |
| Bearer 헤더 없음 | ORDS Handler 필수값 차단. 예: `ORA-20101` |
| 잘못된 key | key hash 매핑 실패. 예: `ORA-20002` |

### 8.5 VPD 결과

![VPD 결과 화면](assets/agent-ords-security-vpd-screen.svg)

확인 항목:

| 항목 | 의미 |
|---|---|
| DB user | Oracle 세션 사용자 |
| Key User | Bearer Key가 매핑한 실제 권한 사용자 |
| 행(row) 제한 | 권한 테이블과 VPD 함수로 적용 |
| 민감 컬럼 값 처리 | Redaction으로 NULL/마스킹 |

### 8.6 DDS 결과

![DDS 결과 화면](assets/agent-ords-security-dds-screen.svg)

확인 항목:

| 항목 | 의미 |
|---|---|
| DDS user | `END USER` |
| DATA ROLE | 권한 묶음 |
| DATA GRANT | 보호 객체, 행(row), 컬럼(column) 허용/제외 |
| 권한 미부여 | `ORA-00942`, 객체 자체가 보이지 않음 |

### 8.7 중앙 권한 인벤토리

![중앙 권한 인벤토리 화면](assets/agent-ords-security-audit-screen.svg)

확인 항목:

| 항목 | 의미 |
|---|---|
| VPD policy | 어느 보호 객체에 어떤 정책 함수가 붙었는지 |
| Redaction policy | 어떤 컬럼이 NULL 또는 마스킹 대상인지 |
| DDS DATA GRANT | End User/DATA ROLE별 행/컬럼 권한 |
| 누락 확인 | 보호 객체 목록과 Dictionary 조회 결과를 비교 |

</details>
