# 설계서: DDS MCP END USER Context 및 런타임 권한 판정 (#617)

> **상태**: Context 경로 Implemented · 런타임 권한 함수 설계 Approved, 이행 Pending
> **최종수정**: 2026-07-19
> **추적성** — Redmine: #617 · 관련 ADR: [ADR-0001](../../adr/0001-dds-mcp-service-identity.md), [ADR-0002](../../adr/0002-dds-runtime-permission-source.md)
> · 현재 구현: `DdsMcpBearerAuthenticator`, `DdsMcpEndUserResolver`, `DdsMcpContextExecutor`, `DdsMcpSseController`
> · 기존 PoC 게시: `sql/adb/44_dds_mcp_local_end_user_setup.sql`
> · 검증: `sql/adb/45_dds_mcp_local_end_user_test.sql`, SSE `tools/call`

## 1. 결정과 목적

MCP Tool의 보호 SQL은 요청 Bearer가 지정한 업무 사용자의 local DDS `END USER` Context에서만 실행한다. OCI IAM Confidential Application은 이 Context attach를 승인하는 서비스 신원이며, 사람 사용자 계정을 IAM에 만들 필요는 없다.

업무 사용자·그룹·역할·퍼미션(`CB_APP_USER`, `CB_USER_ROLE`, `CB_USER_GROUP`, `CB_GROUP_ROLE`, `CB_PERMISSION`, 규칙·컬럼 테이블)은 권한의 단일 원천이다. DDS는 이 원천을 복제한 사용자별 Grant를 매 변경마다 재생성하는 대신, 보호 Object/행/컬럼 그룹별로 미리 만든 `DATA GRANT`가 DB 소유 권한 판정 함수를 호출하도록 한다.

따라서 **역할·퍼미션 변경은 다음 SQL부터 반영되고 DDS DDL 재게시가 필요 없다.** 새 보호 Object·새 컬럼·새 CRUD 동작을 등록하는 경우에만 안전하게 검토한 DDL을 한 번 provisioning한다.

## 1.1 2026-07-19 현행 상태

- 완료: OCI IAM Confidential Application 기반 database-access token, ADB IAM 설정, local DDS END USER Context attach, SSE MCP `tools/call`의 보호 SQL 실행과 context clear를 실제 환경에서 검증했다.
- 완료: 현재 PoC는 `DDS_U_<id>` local END USER와 사용자별 벡터 `DATA GRANT`를 생성하고, 권한 변경 시 bulk 재게시하는 형태로 동작한다.
- 설계 확정: 최종 권한 집행은 사용자별 Grant 복제가 아니라 `DDS_MCP_AUTHZ_PKG.can_access`가 기존 `CB_*` 권한 테이블을 런타임 조회하는 방식으로 전환한다.
- 미구현: 공통 `DDS_MCP_RUNTIME` DATA ROLE, Object/CRUD/컬럼 그룹별 장기 `DATA GRANT`, 런타임 권한 함수, 기존 bulk publish 제거, 권한 변경 직후 반영 regression.
- 운영 기준: 사용자·그룹·역할·퍼미션 변경은 DDL 대상이 아니며, 새 보호 Object·컬럼·CRUD 등록만 provisioning 대상이다.

## 2. 토큰과 사용자 — 반드시 구분할 것

| 값 | 역할 | 사람 사용자인가 |
|---|---|---|
| MCP 요청 Bearer | 해시·만료·회수 검증 후 `CB_APP_USER`를 결정 | 예. 업무 사용자 매핑 기준 |
| local DDS END USER | `DDS_U_<CB_APP_USER.user_id>` | 예. DDS가 집행하는 보안 주체 |
| OCI IAM database-access token | Confidential Application의 client-credentials로 Context attach를 승인 | 아니오. 서비스 애플리케이션 신원 |
| OCI IAM end-user token | 향후 authorization-code/OBO 확장 | 현재 MCP 인증 입력으로 사용하지 않음 |

client-credentials token의 `sub`/`client_id`는 서비스 애플리케이션이다. 이를 업무 사용자로 쓰면 모든 요청이 같은 사람 권한으로 해석된다. MCP Bearer 검증 결과와 local END USER 매핑이 업무 사용자를 결정한다.

## 3. 목표 아키텍처

```text
권한 원천 (같은 Oracle DB의 별도 관리 테이블)
  CB_APP_USER + 직접/그룹 역할 + CB_PERMISSION + RULE + COLUMN
                 ↑
                 │ 함수가 매 보호 SQL에서 읽음
  ADMIN.DDS_MCP_AUTHZ_PKG.can_access(...)
                 ↑
고정 DDS DATA GRANT ── Object/CRUD/컬럼 그룹별 1회 provisioning
  ON ADMIN.CUSTOMER
  AS SELECT (CUSTOMER_ID, CUSTOMER_NAME)
  WHERE can_access(ORA_END_USER_CONTEXT.username,
                   'ADMIN.CUSTOMER', 'SELECT', 'BASIC', ...) = 1
  TO DDS_MCP_RUNTIME
                 ↑
local END USER DDS_U_<id> ── GRANT DATA ROLE DDS_MCP_RUNTIME
                 ↑
MCP Bearer → CB_APP_USER → EndUserSecurityContext attach
```

`object`, `action`, `column group`은 DATA GRANT 정의에 넣는 검증된 상수다. DDS `WHERE`에 현재 조회 중인 Object 명을 자동 전달하는 내장 인자는 없다. 함수의 업무 사용자 인자는 클라이언트가 보낸 ID가 아니라 `ORA_END_USER_CONTEXT.username`이며, 함수가 `CB_DDS_END_USER_MAP`을 통해 실제 `user_id`로 해석한다.

## 4. 권한 모델: 동적 범위와 정적 범위

| 구분 | 결정 위치 | 권한 변경 시 DDL |
|---|---|---|
| 업무 사용자 활성·역할·그룹·퍼미션 | `CB_*` 권한 테이블, `can_access`/`EXISTS` | 불필요 — 다음 쿼리부터 반영 |
| 행 범위: 본인·테넌트·부서·상태·태그 | DATA GRANT `WHERE`와 Context/함수 인자 | 불필요 |
| 컬럼 노출/수정 | Object·CRUD·컬럼 그룹별 DATA GRANT | 퍼미션 값 변경은 불필요. 새 컬럼/새 그룹만 provisioning |
| Object·View·CRUD 등록 | 안전 검토한 DATA GRANT DDL | 필요 |
| local END USER 등록 | `CREATE END USER`, 공통 `DDS_MCP_RUNTIME` Role 부여 | 신규 사용자 최초 1회 |

예를 들어 `PHONE`, `EMAIL`의 열 그룹에는 다음처럼 **한 번만** Grant를 만든다.

```sql
CREATE DATA GRANT admin.customer_contact_read
  AS SELECT (phone, email)
  ON admin.customer
  WHERE admin.dds_mcp_authz_pkg.can_access(
          ORA_END_USER_CONTEXT.username,
          'ADMIN.CUSTOMER', 'SELECT', 'CONTACT', tenant_id, customer_type
        ) = 1
  TO dds_mcp_runtime;
```

권한 테이블에서 해당 사용자의 `CONTACT` 읽기 권한을 제거하면 함수가 `0`을 반환하므로 즉시 `PHONE`과 `EMAIL`은 노출되지 않는다. 같은 Object의 기본 컬럼은 별도 Grant로 정의한다. DDS Grant는 합산(additive)되므로 `AS SELECT` 같은 광범위 Grant를 공통 Role에 만들면 안 되며, 컬럼 그룹은 겹치지 않게 관리한다.

`WHERE`는 행별 true/false를 판단한다. 따라서 함수가 컬럼 목록을 반환하거나 클라이언트의 Object/컬럼 문자열로 동적 SQL을 만드는 구조는 사용하지 않는다. 함수는 고정 Object·컬럼 그룹의 권한과 행 조건만 판정한다.

## 5. 함수와 데이터 경계

`DDS_MCP_AUTHZ_PKG.can_access`는 `AUTHID DEFINER` 패키지로 구현하고 다음을 지킨다.

1. `ORA_END_USER_CONTEXT.username` → `CB_DDS_END_USER_MAP` → 활성 `CB_APP_USER`를 해석한다. MCP 파라미터의 사용자 ID·테넌트 ID를 신뢰하지 않는다.
2. 직접 역할과 활성 그룹 역할, `ALLOW`/`DENY`, Object·동작·컬럼 그룹·행 규칙을 같은 DB의 `CB_*` 테이블에서 평가한다.
3. 미매핑, 비활성, 권한 없음, 예상 밖의 입력, 예외는 모두 `0`으로 처리한다. 예외를 허용으로 바꾸지 않는다.
4. Function의 SQL은 bind 기반 정적 SQL만 사용한다. Object·동작·컬럼 그룹은 DATA GRANT에 적은 whitelist 상수로만 전달하며 동적 SQL을 만들지 않는다.
5. 권한 테이블은 보호 대상 업무 Object와 분리한다. predicate가 대상 Object를 다시 조회하면 DDS 순환 predicate 오류가 발생한다.
6. Grant owner에는 함수와 권한 테이블에 대한 필요한 **직접** 권한만 부여한다. MCP END USER에는 권한 테이블의 직접 조회·수정 권한을 주지 않는다.
7. 즉시 반영이 목적이므로 함수에 `DETERMINISTIC` 또는 임의의 결과 캐시를 붙이지 않는다. 권한 테이블에는 사용자·역할·Object·동작 기준 인덱스를 둔다.

권한 테이블이 다른 DB나 DB link에만 존재하면 이 모델은 사용할 수 없다. DDS predicate는 원격 Object 참조를 지원하지 않는다. 이 경우에는 현행 게시형 동기화나, 동일 DB의 읽기 전용 권한 projection 테이블이 필요하다.

## 6. 전환 계획과 현재 구현의 위치

현재 PoC는 사용자별 `DDS_U_<id>_ROLE` 및 사용자별 벡터 `DATA GRANT`를 생성하고, 권한 변경 시 전체 재게시한다. Context attach·OCI IAM token·Bearer→사용자 매핑·fail-closed clear는 실증 완료된 기반이며 그대로 유지한다.

다음 이행에서 바꿀 부분은 권한 집행 계층이다.

1. `DDS_MCP_RUNTIME` 공통 DATA ROLE과 `CB_DDS_END_USER_MAP` 기반 local END USER provisioning을 만든다.
2. `DDS_MCP_AUTHZ_PKG`와 권한 테이블 인덱스를 만든다.
3. 보호 Object/CRUD/컬럼 그룹별 장기 DATA GRANT를 생성한다. Object/컬럼 메타데이터 변경 때만 이 DDL을 변경한다.
4. `DdsMcpAuthorizationChangeListener`의 사용자별 Grant bulk 재발행을 제거한다. 관리 변경은 권한 테이블 transaction commit만으로 즉시 반영한다.
5. 기존 사용자별 Grant를 회수하고 dictionary·MCP regression으로 default deny, 컬럼 `NULL`, 행 필터, 권한 변경 직후 반영을 검증한다.

수동 publish는 권한 변경 반영 수단이 아니라 최초 provisioning, 스키마 변경, drift 복구와 검증 용도로 남긴다.

## 7. OCI IAM / ADB 전제조건

1. ADB external authentication에 `OCI_IAM` 및 domain/application ID를 설정한다.
2. `OCI_IAM_DOMAIN_DB_CRED$`에 Confidential Application 자격증명을 보관한다.
3. pool account에 `CREATE SESSION`, `CREATE END USER SECURITY CONTEXT`를 부여하고 TLS wallet 연결을 사용한다.
4. application identity를 `IAM_OAUTH_CLIENT_ID=<client id>`로 등록한다.
5. application은 database resource scope의 client-credentials token을 얻는다.

DB는 `resource_app_id`, `tenant_iss`, audience와 scope를 OCI IAM/DB 설정과 비교한다. client ID, secret, raw token, lookup key는 문서·Git·Redmine·로그에 기록하지 않는다.

## 8. 검증 상태와 인수 기준

### 완료된 기반 검증 (2026-07-02)

- [x] SQLcl로 ADB 접속 및 local `END USER`, `DATA ROLE`, `DATA GRANT` catalog 생성 검증.
- [x] OCI IAM database-access token, database credential, application identity mapping 검증.
- [x] `/dds/mcp/sse`, `tools/list`, 실제 `dds_vector_search`의 Bearer → Context attach → 보호 query → clear HTTP 200 검증.

### 런타임 권한 함수 이행 인수 기준

- [ ] `CB_PERMISSION`의 ALLOW/DENY 또는 사용자·그룹 역할을 commit하면 다음 MCP 쿼리 결과가 DDL 없이 바뀐다.
- [ ] Object 권한 제거 시 결과는 default deny(행 없음 또는 접근 불가)다.
- [ ] 컬럼 그룹 권한 제거 시 해당 컬럼만 `NULL`이고, 다른 승인 컬럼·행 범위는 유지된다.
- [ ] 다른 사용자/테넌트/부서의 행은 함수 predicate를 통과하지 않는다.
- [ ] 함수 예외, 매핑 누락, 비활성 사용자는 fail-closed다.
- [ ] A→B→A 및 동시 pooled connection에서 Context와 권한이 섞이지 않는다.

## 9. 보안 불변식

1. 유효한 MCP Bearer 하나는 정확히 하나의 활성 `CB_APP_USER`와 local DDS END USER로만 해석된다.
2. Context attach 이전·clear 이후에는 보호 SQL을 실행하지 않는다.
3. 서비스 token은 Context attach 승인용일 뿐 사용자 권한을 결정하지 않는다.
4. 업무 권한 원천은 `CB_*` 테이블 하나이며 함수와 관리 UI 모두 이를 사용한다.
5. 예외·미매핑·권한 없음은 항상 거부다.
6. secret, raw bearer, database-access token, lookup key는 응답·로그·Git·Redmine에 기록하지 않는다.

## 10. 참고

- [Oracle: End-User Security Context](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/end-user-security-context.html)
- [Oracle: Create Data Grants](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/create-data-grants.html)
- [Oracle: About Data Grants](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/data-grants.html)
- [Oracle: Configure the Database for IAM Integration](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/configure-database-iam-integration.html)
