Pivot scenario from region-based 2 users to source-based 4 users

Replaces the original APAC-vs-all 2-user demo (vpduser_a/b on
KR_ANALYSTS/GLOBAL_ADMINS groups) with a 2x2 source-access matrix:

  vpduser_my    -> MY_ONLY      group  -> MySQL view only
  vpduser_pg    -> PG_ONLY      group  -> Postgres view only
  vpduser_both  -> BOTH_SOURCES group  -> both views
  vpduser_none  -> (no group)          -> nothing (default deny)

Why: source-level segmentation is the more common production
permission story than region-level filtering. Region filtering
remains available as an opt-in variant via commented UPDATE in
sql/adb/03_seed.sql.

Key changes:
- 03_seed.sql, 07_end_users.sql, 00_cleanup.sql, .env.example,
  run.sh updated for the new 4-user model. All 4 users get
  identical view GRANTs; the only differentiator is the
  permission table (proves the model is "data-driven, not
  GRANT-driven").
- 08-11 split into one file per user: my (+ 5 bypass attempts),
  pg, both, none (default-deny verification).
- 12_tests_admin_audit.sql uses LEFT JOIN so vpduser_none shows
  up as NULL permissions, and filters by object_owner=USER to
  exclude cross-schema policies.
- Removed inline "-- comment" after ";" lines in 03_seed.sql:
  SQL*Plus silently skipped the inserts (documented gotcha).
- README.md + docs/01,02 updated for the 4-user matrix. docs/03
  detailed guide keeps the region-filter example but now has a
  preface explaining it's a variant of the default 4-user model.
- docs/04: db_type='mysql_community' note added (RDS MySQL).

E2E verified: PG=0/MY=17, PG=12/MY=0, PG=12/MY=17, PG=0/MY=0
plus all 5 bypass attempts blocked.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
devmrko
2026-05-26 14:42:11 +09:00
parent 68d53dc5a9
commit ed91306ee3
17 changed files with 424 additions and 191 deletions

View File

@@ -53,8 +53,8 @@ $EDITOR .env
| `PG_HOST` 등 | 원격 Postgres 연결정보 |
| `MY_HOST` 등 | 원격 MySQL 연결정보 |
VPDUSER A/B 의 패스워드는 데모용 기본값이 들어있으니 그대로 써도 무방하지만,
공유 환경이면 바꿔주세요.
VPDUSER (`my` / `pg` / `both` / `none`) 의 패스워드는 데모용 기본값이 들어있으니 그대로
써도 무방하지만, 공유 환경이면 바꿔주세요.
---
@@ -71,7 +71,7 @@ VPDUSER A/B 의 패스워드는 데모용 기본값이 들어있으니 그대로
[ OK ] Postgres source 준비 완료
[ OK ] MySQL source 준비 완료
[ OK ] ADB setup 완료
[ OK ] user_a / user_b 테스트 실행 완료 (위 출력에서 행 수 / 거부 결과 확인)
[ OK ] 4명 (MY / PG / BOTH / NONE) 테스트 실행 완료 (위 출력에서 행 수 / 거부 결과 확인)
[ OK ] audit 완료
[ OK ] === ALL DONE — VPD POC 전체 파이프라인 통과 ===
```
@@ -79,9 +79,15 @@ VPDUSER A/B 의 패스워드는 데모용 기본값이 들어있으니 그대로
이후 직접 검증해보고 싶으면:
```bash
# vpduser_a 로 접속 → APAC rows 만 보여야 함
sqlplus vpduser_a/${VPDUSER_A_PASSWORD}@${ADB_TNS}
SQL> SELECT region, COUNT(*) FROM admin.v_customers_pg GROUP BY region;
# vpduser_pg 로 접속 → PG 뷰는 전부 보이고 MY 뷰는 0 rows 여야 함
sqlplus vpduser_pg/${VPDUSER_PG_PASSWORD}@${ADB_TNS}
SQL> SELECT COUNT(*) FROM admin.v_customers_pg; -- 12
SQL> SELECT COUNT(*) FROM admin.v_customers_my; -- 0
# vpduser_none 으로 접속 → 양쪽 뷰 모두 0 rows (default deny)
sqlplus vpduser_none/${VPDUSER_NONE_PASSWORD}@${ADB_TNS}
SQL> SELECT COUNT(*) FROM admin.v_customers_pg; -- 0
SQL> SELECT COUNT(*) FROM admin.v_customers_my; -- 0
```
---

View File

@@ -43,7 +43,8 @@ LOGON 시 한 번만 읽으므로 쿼리 부하 0.
```
+---------------------+
| vpduser_a / b | ← 사용자 로그인
| vpduser_my / pg / | ← 사용자 로그인
| both / none |
+----------+----------+
|
(1) LOGON 트리거
@@ -88,7 +89,7 @@ LOGON 시 한 번만 읽으므로 쿼리 부하 0.
|---|---|
| `app_customer` | 도메인의 "고객사" 개념 (멀티 테넌트 hook) |
| `app_user` | DB 유저 → 도메인 유저 매핑 (`db_username` 컬럼이 핵심) |
| `app_group` | 그룹 (KR_ANALYSTS, GLOBAL_ADMINS, ...) |
| `app_group` | 그룹 (MY_ONLY, PG_ONLY, BOTH_SOURCES, ...) |
| `user_group` | user ↔ group N:N |
| `db_source` | 원본 소스 식별자 (PG, MY) |
| `permission` | (group, source, region) 행 — `region='*'` 이면 전체 허용 |
@@ -112,7 +113,7 @@ LOGON 시 한 번만 읽으므로 쿼리 부하 0.
4. 그 외 → `RETURN 'region IN (''APAC'',''EMEA'')'` 형식으로 in-list
→ 컨텍스트는 **Secure Application Context** (`USING ctx_pkg`) 라 다른 패키지에서
설정 불가. `vpduser_a` `DBMS_SESSION.SET_CONTEXT('VPD_CTX', ...)` 직접 호출 →
설정 불가. 엔드유저`DBMS_SESSION.SET_CONTEXT('VPD_CTX', ...)` 직접 호출 →
ORA-01031.
---
@@ -134,8 +135,8 @@ ORA-01031.
| 누가 | 무엇을 할 수 있나 |
|---|---|
| `ADMIN` (ADB 관리자) | 전부 다. 정책 BYPASS. **운영에선 절대 일반 사용자에게 주지 말 것** |
| `vpduser_a/b` | (a) 자기에게 GRANT 된 뷰 SELECT, (b) `ctx_pkg.init` 호출 |
| `vpduser_a/b`**못** 하는 것 | DBMS_RLS 변경, EXEMPT ACCESS POLICY, CREATE TABLE (스냅샷 방지), 매핑 테이블 직접 SELECT, DB Link 직접 SELECT |
| `vpduser_*` (my/pg/both/none) | (a) 자기에게 GRANT 된 뷰 SELECT, (b) `ctx_pkg.init` 호출 |
| `vpduser_*`**못** 하는 것 | DBMS_RLS 변경, EXEMPT ACCESS POLICY, CREATE TABLE (스냅샷 방지), 매핑 테이블 직접 SELECT, DB Link 직접 SELECT |
→ 즉 엔드유저 입장에서 정책을 우회할 표면이 없습니다. `07_end_users.sql`
주석에서 "what we are deliberately NOT granting" 섹션이 그 목록.

View File

@@ -3,6 +3,13 @@
> **이 문서의 대상 독자:** 데이터베이스 보안이나 Oracle 기술에 익숙하지 않은 분.
> 용어가 나올 때마다 풀어서 설명하고, 왜 그 장치가 필요한지를 함께 적습니다.
> **시나리오 안내:** 현재 `./run.sh` 가 자동으로 깔아주는 기본 시나리오는 4명의
> 엔드유저 (`vpduser_my`/`pg`/`both`/`none`) 가 각각 다른 소스에 접근 가능한 **소스
> 단위 매트릭스** 입니다 (README 참고). 본 상세 가이드는 그 위에 얹을 수 있는 **행
> 단위 region 필터링** 변형(`KR_ANALYSTS → APAC`, `GLOBAL_ADMINS → '*'`) 을 예시로
> 사용합니다 — VPD 메커니즘 자체는 동일하므로 개념 이해에는 차이가 없습니다.
> region 필터를 실제로 켜려면 `sql/adb/03_seed.sql` 하단의 주석을 해제하세요.
---
## 목차
@@ -470,24 +477,35 @@ AUDIT POLICY vpd_view_access;
## 9. 디렉터리·파일 구조
```
ords/
├── .env ← ADB 연결 정보 (비밀번호 포함, .gitignore 됨)
├── .gitignore ← .env, *.log 제외
├── README.md ← (선택) 실행 가이드
├── run_poc.sh ← 셋업/테스트/감사 일괄 실행 스크립트
vpd-permission-poc/
├── .env ← ADB + RDS 연결 정보 (.gitignore 됨)
├── .env.example
├── README.md ← 클론-앤-고 가이드
├── run.sh ← 원클릭 오케스트레이터
├── scripts/lib/common.sh
├── sql/
│ ├── 00_cleanup.sql ← 초기화 (멱등성)
│ ├── 01_perm_tables.sql 권한 모델 6개 테이블 생성
├── 02_seed.sql ← 데모 데이터 주입
── 03_secure_ctx.sql ← ctx_pkg + Secure Context 생성
│ ├── 04_views.sql ← 외부 테이블에 대한 로컬 뷰 2개
│ ├── 05_policy.sql ← VPD 정책 함수 + ADD_POLICY
│ ├── 06_end_users.sql ← 일반 사용자 2명 + LOGON 트리거
│ ├── 07_tests_user_a.sql ← VPDUSER_A 입장에서 검증
│ ├── 08_tests_user_b.sql ← VPDUSER_B 입장에서 검증
└── 09_tests_admin_audit.sql ← ADMIN 입장에서 정책·권한 현황 감사
│ ├── source/
│ ├── postgres_setup.sql ← 원격 PG: customers + 12 rows
│ └── mysql_setup.sql ← 원격 MySQL: customers + 12 rows
── adb/
├── 00_cleanup.sql ← 멱등 초기화
├── 01_dblinks.sql ← DB Link + credential
├── 02_perm_tables.sql ← 권한 매핑 6개 테이블
├── 03_seed.sql ← 4-user 매트릭스 시드
├── 04_secure_ctx.sql ctx_pkg + Secure Context
├── 05_views.sql ← DB Link 통합 뷰
│ ├── 06_policy.sql ← VPD 정책 + 정책 함수
│ ├── 06a_redaction.sql ← Data Redaction (이메일/이름 마스킹)
│ ├── 07_end_users.sql ← 4 유저 + LOGON 트리거
│ ├── 08_tests_user_my.sql ← MY only + 우회 시도 5종
│ ├── 09_tests_user_pg.sql ← PG only
│ ├── 10_tests_user_both.sql ← both
│ ├── 11_tests_user_none.sql ← default deny 검증
│ └── 12_tests_admin_audit.sql
└── docs/
── 설명.md ← (이 문서)
── 01-quickstart.md
├── 02-architecture.md
└── 03-detailed-guide.md ← (이 문서)
```
---
@@ -495,18 +513,18 @@ ords/
## 10. 실행 방법
```bash
cd /Users/joungminko/devkit/ords
cd vpd-permission-poc
# 1) 전체 셋업 + 테스트 + 감사 한 번에
./run_poc.sh all
# 1) 전체 파이프라인 한 번에
./run.sh # = ./run.sh all
# 또는 단계별로:
./run_poc.sh setup # 정리 + 모든 객체 생성
./run_poc.sh test # VPDUSER_A, VPDUSER_B 시점 테스트
./run_poc.sh audit # ADMIN 시점 정책·권한 감사
./run_poc.sh teardown # 전부 삭제
# .env 파일이 있어야 합니다. (이 POC 에는 포함됨)
./run.sh prereq # 도구/.env 검증
./run.sh source # 원격 PG + MySQL 에 customers 테이블/seed
./run.sh adb # ADB 측 cleanup → dblink → 권한/뷰/정책 → 4 유저
./run.sh tests # 4 명 (MY/PG/BOTH/NONE) 시점 테스트
./run.sh audit # ADMIN 시점 정책·권한 감사
./run.sh teardown # ADB 측 객체 + dblink 정리
```
### 사람의 눈으로 직접 확인하고 싶을 때
@@ -514,15 +532,21 @@ cd /Users/joungminko/devkit/ords
```bash
source .env
# VPDUSER_A로 직접 들어가서 쿼리해보기:
sqlplus "vpduser_a/\"RowFilter#A2026\"@$ADB_TNS"
# 기본 시나리오 — vpduser_pg: PG 뷰는 다 보이고 MySQL 뷰는 0 rows
sqlplus "vpduser_pg/\"${VPDUSER_PG_PASSWORD}\"@$ADB_TNS"
SQL> SELECT COUNT(*) FROM admin.v_customers_pg; -- 12
SQL> SELECT COUNT(*) FROM admin.v_customers_my; -- 0
# vpduser_none — default deny
sqlplus "vpduser_none/\"${VPDUSER_NONE_PASSWORD}\"@$ADB_TNS"
SQL> SELECT COUNT(*) FROM admin.v_customers_pg; -- 0
SQL> SELECT COUNT(*) FROM admin.v_customers_my; -- 0
# region 필터 변형을 켰을 때 — vpduser_both 에게 APAC 만 허용한 경우
# (sql/adb/03_seed.sql 하단 UPDATE 주석 해제 후)
sqlplus "vpduser_both/\"${VPDUSER_BOTH_PASSWORD}\"@$ADB_TNS"
SQL> SELECT region, COUNT(*) FROM admin.v_customers_pg GROUP BY region;
-- 결과: APAC 만 보임
# VPDUSER_B로:
sqlplus "vpduser_b/\"RowFilter#B2026\"@$ADB_TNS"
SQL> SELECT region, COUNT(*) FROM admin.v_customers_pg GROUP BY region;
-- 결과: APAC, EMEA, NA 모두 보임
```
---

View File

@@ -41,7 +41,7 @@ seed rows : 12 (PK 101~112, 역시 APAC/EMEA/AMER 4/4/4)
PK 범위를 PG (1~12) 와 다르게 가져간 이유:
* 두 소스는 서로 독립적인 별개의 데이터 라는 것을 데모에서 시각적으로 보여주기 위함
* `vpduser_b` 가 양쪽 뷰를 합칠 때 PK 가 겹치지 않아 UNION 데모하기 쉬움
* `vpduser_both` 가 양쪽 뷰를 합칠 때 PK 가 겹치지 않아 UNION 데모하기 쉬움
수동 실행:
@@ -68,7 +68,8 @@ DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK(
);
```
MySQL 도 동일 패턴, `db_type``'mysql'`.
MySQL 도 동일 패턴, `db_type``'mysql_community'` (AWS RDS MySQL 은 Community 빌드 — 기본
`'mysql'` 은 Enterprise Server 전용이라 `ORA-28500` 으로 거절됨).
링크 검증: