Files
vpd-permission-poc/docs/06-agent-ords-vpd-dds-security-brief.md

1853 lines
80 KiB
Markdown

# 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>