# 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 ` 형식 | `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 `를 받고 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 ` 전달 | 행/컬럼 권한 판단 | | 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 등록 ```
운영 검토 항목 - 적용 단위, 권한 조회, 누락 방지 | 검토 항목 | 기준 | |---|---| | 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 조회 결과를 검증 단계에 포함한다. |
오류 코드 상세 - ORA 코드 해석과 정상 차단 여부 | 오류 코드 | 이 문서에서의 의미 | 정상 차단 여부 | 확인 방법 | |---|---|---|---| | `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 ` 헤더가 없거나 형식이 틀림 | 정상 차단. 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`이 여기에 해당한다. | | 정상 차단 오류 | 보안 검증에서는 실패가 아니라 “우회가 막혔다”는 증거다. 단, 운영 장애 분석에서는 같은 코드라도 발생 위치와 사용자, 대상 객체를 함께 봐야 한다. |
운영 책임 상세 - 누가 무엇을 관리하는지 | 영역 | 담당 역할 | 관리 항목 | |---|---|---| | 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` 같은 컬럼이 그 역할이다.
성능·감사·운영 체크리스트 - 적용 후 점검 항목 | 구분 | 체크 항목 | |---|---| | 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` 결과를 배포 전후 비교 |
--- ## 2. 사용자 식별 및 권한 매핑 구조 이 장에서는 Agent 요청이 DB 권한 적용 대상으로 변환되는 방식을 정리한다. DB User 기반, Bearer Key 기반, DDS END USER 기반을 구분하고, 본 요구사항의 기본 경로는 `DB user != Key User` 구조임을 명확히 한다.
상세 설명 - DB User, Bearer Key, DDS END USER 식별 경로 ### 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 ` 포함 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='; 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...` | 즉, Bearer Key를 DDS에 적용하려면 아래 변환이 필요하다. ```text Authorization: Bearer -> 호출 애플리케이션 계층에서 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 = "" database_access_token = "" # 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에 매핑 가능 |
## 3. 권한 매핑 테이블 설계 이 장에서는 VPD 경로에서 사용할 업무 권한 테이블을 정의한다. 핵심 구조는 `사용자 -> 역할 -> 보호 객체 권한 -> 행 규칙`이며, Bearer Key는 내부 사용자로 매핑되는 식별 수단으로 둔다.
상세 설명 - Key User, Role, Permission, 행 규칙 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` 값으로 이 컬럼을 조회한다.
## 4. VPD 기반 접근 통제 설계 이 장에서는 ORDS가 Bearer Key를 내부 사용자로 매핑한 뒤, DB Session Context와 VPD 정책 함수로 행(row)을 제한하는 방식을 설명한다. 컬럼 값 NULL/마스킹은 VPD가 아니라 Oracle Data Redaction으로 분리해 설명한다.
상세 설명 - SYS_CONTEXT, p_object, EXISTS, Redaction 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 = '' 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; ```
## 5. DDS 기반 접근 통제 설계 이 장에서는 DDS `END USER`, `DATA ROLE`, `DATA GRANT`를 이용해 보호 객체의 행(row), 컬럼(column), 작업(action)을 선언형으로 통제하는 방식을 설명한다. Bearer Key를 DDS에 적용하려면 Key 조회 결과가 `EndUserSecurityContext`로 DB 호출 전에 전파되어야 한다.
상세 설명 - END USER, DATA ROLE, DATA GRANT 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 ` | | 행(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; ```
## 6. VPD/DDS 비교 및 적용 기준 이 장에서는 앞에서 분리 설명한 VPD와 DDS를 운영 기준으로 비교한다. 비교 기준은 사용자 식별 방식, 권한 저장 위치, 행/컬럼 통제 범위, Bearer Key 처리 방식, 적용 단위, 권한 미부여 시 결과로 둔다.
상세 설명 - 운영 선택 기준 | 항목 | 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에 동시에 걸지 말고 별도 보호 객체로 분리 |
## 7. ADB 실행 검증 이 장에서는 ADB에 생성한 로컬 테이블 기준으로 VPD + Redaction, DDS, ORDS Handler Bearer 검증을 실행한 스크립트와 결과를 제시한다.
상세 설명 - 실행 가능한 VPD/DDS 스크립트와 결과 이 예제는 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` | 전체 실행 | | `database/adb/16_agent_ords_security_local_cleanup.sql` | `CB_*` 예제 객체 정리 | | `database/adb/17_agent_ords_security_local_vpd_setup.sql` | VPD용 로컬 테이블, 권한 테이블, context, redaction, 정책 생성 | | `database/adb/18_agent_ords_security_local_vpd_test.sql` | `CB_ORDS`로 Bearer Key 기반 VPD 테스트 | | `database/adb/19_agent_ords_security_local_dds_setup.sql` | DDS용 로컬 테이블, END USER, DATA ROLE, DATA GRANT 생성 | | `database/adb/20_agent_ords_security_local_dds_test.sql` | DDS end user별 조회 테스트 | | `database/adb/21_agent_ords_security_ords_enable_schema.sql` | `CB_ORDS` schema를 ORDS에 enable | | `database/adb/22_agent_ords_security_ords_handler_setup.sql` | ORDS Module/Handler와 Handler Package 생성 | | `database/adb/23_agent_ords_security_ords_handler_test.sql` | Handler Package 직접 실행으로 VPD/DDS Bearer 경로 검증 | | `database/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 === 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로 확인할 수 있다. 이 예제에서는 `database/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 시스템 필요 |
## 8. 결과 화면 및 운영 확인 항목 이 장에서는 고객 설명에 사용할 화면 형태의 예시를 정리한다. 사용자 식별 시나리오, 권한 매핑, Authorization 헤더 처리, VPD 결과, DDS 결과, 중앙 권한 인벤토리를 순서대로 확인한다.
상세 설명 - 시나리오, 권한 등록, 실행 결과 확인 ### 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 조회 결과를 비교 |