From b77966abaf8ede7ef1ad6016bb133902102929ef Mon Sep 17 00:00:00 2001 From: devmrko Date: Fri, 3 Jul 2026 10:05:26 +0900 Subject: [PATCH] [Architect] #617 define runtime DDS authorization --- docs/adr/0001-dds-mcp-service-identity.md | 4 +- .../adr/0002-dds-runtime-permission-source.md | 40 ++++ .../617-dds-mcp-end-user-context/README.md | 198 +++++++++++------- .../fn-authenticate-mcp-bearer.md | 6 +- .../fn-compile-dds-publish-plan.md | 24 +-- .../fn-evaluate-runtime-dds-permission.md | 53 +++++ .../fn-publish-and-verify-dds-plan.md | 10 +- .../fn-with-dds-end-user-context.md | 2 +- 8 files changed, 233 insertions(+), 104 deletions(-) create mode 100644 docs/adr/0002-dds-runtime-permission-source.md create mode 100644 docs/design/617-dds-mcp-end-user-context/fn-evaluate-runtime-dds-permission.md diff --git a/docs/adr/0001-dds-mcp-service-identity.md b/docs/adr/0001-dds-mcp-service-identity.md index 4e45e98..b1895c2 100644 --- a/docs/adr/0001-dds-mcp-service-identity.md +++ b/docs/adr/0001-dds-mcp-service-identity.md @@ -4,11 +4,11 @@ ## 결정 -OCI IAM Confidential Application의 client-credentials token을 database-access token으로 사용한다. MCP 요청자는 기존 opaque Bearer → `CB_APP_USER` 매핑으로 판정하고, 그 사용자를 local DDS `END USER`로 attach한다. +OCI IAM Confidential Application의 client-credentials token을 database-access token으로 사용한다. MCP 요청자는 기존 opaque Bearer → `CB_APP_USER` 매핑으로 판정하고, 그 사용자를 local DDS `END USER`로 attach한다. 업무 권한의 런타임 판정 방식은 [ADR-0002](0002-dds-runtime-permission-source.md)를 따른다. ## 근거 -client-credentials token의 `client_id`/`sub`는 서비스 애플리케이션이다. 이를 업무 사용자로 사용하면 모든 MCP 요청이 같은 사람 권한으로 해석되는 오류가 생긴다. 서비스 승인과 업무 사용자 신원을 분리하면 기존 사용자·그룹·권한 관리 모델을 유지하면서 DDS가 사용자별 `DATA ROLE`/`DATA GRANT`를 집행한다. +client-credentials token의 `client_id`/`sub`는 서비스 애플리케이션이다. 이를 업무 사용자로 사용하면 모든 MCP 요청이 같은 사람 권한으로 해석되는 오류가 생긴다. 서비스 승인과 업무 사용자 신원을 분리하면 기존 사용자·그룹·권한 관리 모델을 유지하면서 DDS가 local END USER Context와 `DATA GRANT` predicate로 권한을 집행한다. ## 검증 diff --git a/docs/adr/0002-dds-runtime-permission-source.md b/docs/adr/0002-dds-runtime-permission-source.md new file mode 100644 index 0000000..a57fba2 --- /dev/null +++ b/docs/adr/0002-dds-runtime-permission-source.md @@ -0,0 +1,40 @@ +# ADR-0002: DDS 권한은 기존 권한 테이블을 런타임에 판정한다 + +> **상태**: Accepted · **날짜**: 2026-07-03 · **관련 이슈**: #617 + +## 맥락 + +현재 PoC는 `CB_*` 사용자·그룹·역할·퍼미션 모델을 사용자별 local DDS `DATA ROLE`과 `DATA GRANT`로 게시한다. 권한 변경 뒤 즉시 집행하려면 영향을 받는 모든 사용자의 DDS DDL을 재생성해야 한다. 이는 권한 원천이 두 곳이 되고, 동기화 실패·지연·drift를 만든다. + +DDS `DATA GRANT`의 predicate는 SQL 서브쿼리와 `ORA_END_USER_CONTEXT`를 사용할 수 있다. 컬럼 권한은 Data Grant의 `SELECT (column...)`/`UPDATE (column...)`으로 정적 선언되지만, 그 Grant의 predicate는 매 SQL 실행 때 권한 테이블을 조회할 수 있다. + +## 결정 + +1. `CB_*` 권한 테이블을 업무 권한의 유일한 원천으로 둔다. +2. `ADMIN.DDS_MCP_AUTHZ_PKG`의 definer-rights 함수를 통해 local DDS END USER를 업무 사용자로 해석하고, Object·CRUD·컬럼 그룹·행 속성에 대한 권한을 런타임에 판정한다. +3. DDS에는 보호 Object/CRUD/컬럼 그룹별 장기 `DATA GRANT`를 한 번 provisioning한다. 각 predicate는 `ORA_END_USER_CONTEXT.username`과 DDL에 고정한 whitelist Object·동작·컬럼 그룹을 함수에 전달한다. +4. 모든 MCP END USER에는 공통 `DDS_MCP_RUNTIME` DATA ROLE만 부여한다. 권한 테이블의 값 변경은 DDL 없이 다음 쿼리부터 반영한다. +5. 새 보호 Object·컬럼·CRUD, 또는 컬럼 그룹 재설계는 검토된 DDL provisioning 대상이다. 사용자/그룹/역할/퍼미션 값 변경은 provisioning 대상이 아니다. + +## 결과 + +- 권한 변경의 즉시성은 권한 테이블 transaction commit으로 보장한다. +- Object/컬럼 목록은 여전히 정적이다. 함수는 컬럼 목록을 동적으로 만들거나 클라이언트 제공 Object 명으로 dynamic SQL을 실행하지 않는다. +- 셀 수준 제어는 고정 컬럼 그룹별 Grant와 동적 predicate를 결합한다. 여러 Grant는 additive이므로 광범위한 공통 `SELECT` Grant를 만들지 않는다. +- 함수는 권한 테이블을 보호 대상 Object와 분리해 조회해야 한다. 동일 Object 재조회는 순환 predicate 오류를 만든다. +- 권한 테이블은 같은 Oracle DB에 있어야 한다. DB link/원격 권한 원천은 DDS predicate에서 지원하지 않는다. +- 현재 사용자별 재게시 구현은 전환 기간의 PoC다. 기능 이행과 회귀 검증 후 `DdsMcpAuthorizationChangeListener`의 bulk publish 의존성을 제거한다. + +## 대안 + +### 사용자별 DDS Grant 재게시 + +권한 계산 결과가 DB dictionary에 명시적으로 남고 쿼리 비용이 낮다는 장점이 있다. 그러나 권한 원천을 복제하고, 변경마다 bulk DDL·실패 복구·drift 관리가 필요해 채택하지 않는다. + +### MCP Tool이 권한을 판정한 뒤 SQL을 선택 + +애플리케이션 버그·우회 경로·새 Tool이 데이터 보호를 무력화할 수 있어 채택하지 않는다. 판정은 보호 SQL과 같은 DB에서 DDS predicate로 집행한다. + +### 컬럼별 동적 View 마스킹 + +View의 `CASE`와 함수로 구현할 수 있으나, 원본 Object 차단·View 권한·함수 성능을 별도로 보장해야 한다. DDS 컬럼 Grant가 필요한 기본 경로의 대체안으로 사용하지 않는다. diff --git a/docs/design/617-dds-mcp-end-user-context/README.md b/docs/design/617-dds-mcp-end-user-context/README.md index 22a5860..83982ad 100644 --- a/docs/design/617-dds-mcp-end-user-context/README.md +++ b/docs/design/617-dds-mcp-end-user-context/README.md @@ -1,110 +1,148 @@ -# 설계서: DDS MCP 사용자별 END USER Context 및 권한 게시 (#617) +# 설계서: DDS MCP END USER Context 및 런타임 권한 판정 (#617) -> **상태**: Implemented — OCI IAM·ADB·SSE 실증 완료 -> **최종수정**: 2026-07-02 -> **추적성** — Redmine: #617 · 관련 ADR: [ADR-0001](../../adr/0001-dds-mcp-service-identity.md) -> · 구현: `DdsMcpBearerAuthenticator`, `DdsMcpEndUserResolver`, `DdsMcpContextExecutor`, `DdsMcpSseController` -> · DB 게시: `sql/adb/44_dds_mcp_local_end_user_setup.sql` +> **상태**: Context 경로 Implemented · 런타임 권한 함수 설계 Approved, 이행 Pending +> **최종수정**: 2026-07-03 +> **추적성** — 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에서만 실행한다. DDS가 `DATA ROLE`과 `DATA GRANT`로 행·컬럼 접근을 집행하며, Tool은 권한 predicate를 직접 만들지 않는다. +MCP Tool의 보호 SQL은 요청 Bearer가 지정한 업무 사용자의 local DDS `END USER` Context에서만 실행한다. OCI IAM Confidential Application은 이 Context attach를 승인하는 서비스 신원이며, 사람 사용자 계정을 IAM에 만들 필요는 없다. -VPD 경로는 변경하지 않는다. 기존 DDS 관리의 권한 게시 경계는 MCP용 END USER/역할/grant도 함께 갱신한다. +업무 사용자·그룹·역할·퍼미션(`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한다. ## 2. 토큰과 사용자 — 반드시 구분할 것 -| 값 | 현재 구현에서의 역할 | 사람 사용자인가 | +| 값 | 역할 | 사람 사용자인가 | |---|---|---| -| MCP 요청 Bearer | `CB_AGENT_BEARER_KEY` 해시·만료·회수 검증 후 `CB_APP_USER`를 결정 | **예.** DB의 업무 사용자 매핑 기준 | -| local DDS END USER | `DDS_U_` | **예.** DDS가 집행하는 보안 주체 | -| OCI IAM database-access token | Confidential application의 client-credentials로 매 Tool Context attach를 승인 | 아니오. 서비스 애플리케이션 신원 | -| OCI IAM end-user token | 향후 authorization-code/OBO 전용 확장 | 현재 MCP 인증 입력으로 사용하지 않음 | +| MCP 요청 Bearer | 해시·만료·회수 검증 후 `CB_APP_USER`를 결정 | 예. 업무 사용자 매핑 기준 | +| local DDS END USER | `DDS_U_` | 예. DDS가 집행하는 보안 주체 | +| OCI IAM database-access token | Confidential Application의 client-credentials로 Context attach를 승인 | 아니오. 서비스 애플리케이션 신원 | +| OCI IAM end-user token | 향후 authorization-code/OBO 확장 | 현재 MCP 인증 입력으로 사용하지 않음 | -따라서 현재 MCP Bearer가 `CB_APP_USER`를 지정할 수 있으면 그 사용자별 DDS 권한은 적용된다. 반면 client-credentials 토큰의 `sub`/`client_id`는 서비스 애플리케이션이므로 사람 사용자를 판정하는 데 사용하면 안 된다. +client-credentials token의 `sub`/`client_id`는 서비스 애플리케이션이다. 이를 업무 사용자로 쓰면 모든 요청이 같은 사람 권한으로 해석된다. MCP Bearer 검증 결과와 local END USER 매핑이 업무 사용자를 결정한다. -OCI IAM JWT의 사람 사용자 claim을 그대로 DDS에 전달하려면 별도의 authorization-code 또는 OBO flow, 해당 end-user token 검증, IAM role↔DDS DATA ROLE 매핑으로 확장해야 한다. 이 경로는 현재 local END USER 모델과 혼용하지 않는다. - -## 3. 실제 아키텍처 +## 3. 목표 아키텍처 ```text -권한 관리 / DDS 게시 - CB_APP_USER + 직접/그룹 역할 + CB_PERMISSION 규칙 - → DDS Publisher - → CB_DDS_END_USER_MAP - → CREATE END USER DDS_U_ - → CREATE/GRANT DATA ROLE DDS_U__ROLE - → CREATE DATA GRANT (보호 벡터 객체) - -MCP SSE Tool 호출 - Authorization: Bearer - → 매 메시지마다 bearer 검증 - → CB_APP_USER 확인 - → 게시된 DDS_U_ map 확인 - → OCI IAM database-access token 취득/캐시 - → EndUserSecurityContext(name, lookup key) attach - → 보호 SQL 실행 (DATA GRANT 집행) - → finally: context clear, 연결 반환 +권한 원천 (같은 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_ ── GRANT DATA ROLE DDS_MCP_RUNTIME + ↑ +MCP Bearer → CB_APP_USER → EndUserSecurityContext attach ``` -SSE 연결 자체는 인증을 보조할 뿐이다. `/dds/mcp/messages`의 **매 요청마다** Bearer와 사용자를 재검증하므로, 연결을 오래 유지해도 이전 사용자 권한을 신뢰하지 않는다. +`object`, `action`, `column group`은 DATA GRANT 정의에 넣는 검증된 상수다. DDS `WHERE`에 현재 조회 중인 Object 명을 자동 전달하는 내장 인자는 없다. 함수의 업무 사용자 인자는 클라이언트가 보낸 ID가 아니라 `ORA_END_USER_CONTEXT.username`이며, 함수가 `CB_DDS_END_USER_MAP`을 통해 실제 `user_id`로 해석한다. -## 4. 게시 모델과 운영 규칙 +## 4. 권한 모델: 동적 범위와 정적 범위 -- 활성 `CB_APP_USER`마다 `CB_DDS_END_USER_MAP`에 `DDS_U_`, `DDS_U__ROLE`, grant 이름과 게시 상태를 기록한다. -- local END USER Context의 lookup key는 서비스 secret과 사용자 ID로 HMAC 파생한다. 원문/파생값은 DB·Git·Redmine·로그에 저장하지 않는다. -- 사용자의 직접 역할과 활성 그룹 역할에서 `CB_PERMISSION` / `CB_PERMISSION_RULE` / 허용 컬럼을 계산해 벡터 보호 객체의 `DATA GRANT`를 재생성한다. -- ALLOW가 없으면 grant를 제거하여 DDS 기본 거부 상태를 유지한다. 비활성 사용자는 data role과 grant를 회수한다. -- 사용자 활성화·직접 역할·그룹 멤버/역할·permission rule/허용 컬럼 변경은 같은 요청 안에서 활성 사용자 전체를 bulk 재발행한다. 수동 **DDS 관리의 게시 작업**은 전체 복구·재검증용으로도 제공한다. -- `withDataRoles(...)`는 local username+lookup-key Context에 사용하지 않는다. 역할은 게시된 local END USER grant에서만 활성화된다. - -## 5. OCI IAM / ADB 전제조건 - -1. ADB external authentication을 `OCI_IAM`으로 등록하고 application ID와 domain URL을 설정한다. -2. `OCI_IAM_DOMAIN_DB_CRED$` credential에 Confidential application의 client ID/secret을 보관한다. -3. pool account에 `CREATE SESSION`, `CREATE END USER SECURITY CONTEXT`를 부여하고 TLS wallet 연결을 사용한다. -4. application identity를 `IAM_OAUTH_CLIENT_ID=`로 등록한다. 서비스 역할을 application identity에 부여할 경우 해당 역할만 활성화된다. -5. application은 database resource scope로 client-credentials token을 얻는다. - -DB가 확인하는 token claim은 `resource_app_id`, `tenant_iss`, audience와 scope다. 이 값은 DB OCI IAM 설정 및 database resource registration과 일치해야 한다. - -## 6. 구현 경계 - -| 컴포넌트 | 책임 | 실패 처리 | +| 구분 | 결정 위치 | 권한 변경 시 DDL | |---|---|---| -| `DdsMcpBearerAuthenticator` | MCP Bearer → 활성 `CB_APP_USER` | 인증 실패, SQL 미실행 | -| `DdsMcpEndUserResolver` | 사용자 → 게시된 DDS principal | 미매핑/미게시면 거부 | -| `DdsMcpDatabaseAccessTokenProvider` | OCI IAM service token 발급·만료 전 갱신 | Context 실행 차단 | -| `DdsMcpContextExecutor` | attach → 제한된 SQL → clear | clear 실패 시 연결 폐기 | -| `DdsMcpVectorSearchService` | Context 내부 보호 벡터 SQL만 실행 | DDS 오류를 안전한 MCP 오류로 변환 | -| `DdsMcpAuthorizationChangeListener` | 권한 변경 event → 전체 local END USER grant 재발행 | 요청을 실패로 알리고 게시 상태를 확인하게 함 | -| DDS Publisher | 기존 권한 모델 → local END USER/DATA ROLE/DATA GRANT | 부분 실패를 게시 실패로 기록 | +| 업무 사용자 활성·역할·그룹·퍼미션 | `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회 | -Tool/Repository는 raw Bearer, client secret, lookup key 또는 `setEndUserSecurityContext`를 직접 다루지 않는다. +예를 들어 `PHONE`, `EMAIL`의 열 그룹에는 다음처럼 **한 번만** Grant를 만든다. -## 7. 검증 결과 (2026-07-02) +```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__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=`로 등록한다. +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 발급 성공. `resource_app_id`, `tenant_iss`, audience `DDSDB`, scope `DB_ACCESS_SCOPE`가 DB 설정과 일치. -- [x] DB credential `OCI_IAM_DOMAIN_DB_CRED$`, OCI IAM application identity mapping 확인. -- [x] `/dds/mcp/sse` endpoint event 및 `tools/list` HTTP 200 확인. -- [x] `dds_vector_search` tool call: Bearer → application user 101 → local DDS Context attach → 보호 query 3행 반환 → context clear, HTTP 200. -- [ ] 사용자 A→B→A 및 동시 요청의 full isolation regression을 자동화한다. -- [ ] DDS 관리 UI publish가 MCP END USER map/grant를 같은 트랜잭션 경계에서 갱신하도록 통합한다. +- [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 검증. -SSE stream은 연결을 유지하므로 client read timeout이 날 수 있다. 이는 실패 판정 기준이 아니며, Tool RPC의 HTTP 결과와 attach/query/clear 로그로 성공을 판정한다. +### 런타임 권한 함수 이행 인수 기준 -## 8. 보안 불변식 +- [ ] `CB_PERMISSION`의 ALLOW/DENY 또는 사용자·그룹 역할을 commit하면 다음 MCP 쿼리 결과가 DDL 없이 바뀐다. +- [ ] Object 권한 제거 시 결과는 default deny(행 없음 또는 접근 불가)다. +- [ ] 컬럼 그룹 권한 제거 시 해당 컬럼만 `NULL`이고, 다른 승인 컬럼·행 범위는 유지된다. +- [ ] 다른 사용자/테넌트/부서의 행은 함수 predicate를 통과하지 않는다. +- [ ] 함수 예외, 매핑 누락, 비활성 사용자는 fail-closed다. +- [ ] A→B→A 및 동시 pooled connection에서 Context와 권한이 섞이지 않는다. -1. 유효한 MCP Bearer 하나는 정확히 하나의 활성 `CB_APP_USER`와 하나의 게시된 DDS END USER로만 해석된다. +## 9. 보안 불변식 + +1. 유효한 MCP Bearer 하나는 정확히 하나의 활성 `CB_APP_USER`와 local DDS END USER로만 해석된다. 2. Context attach 이전·clear 이후에는 보호 SQL을 실행하지 않는다. -3. attach/query/clear 어느 단계가 실패해도 fail-closed한다. -4. client-credentials token은 서비스 승인용이며 업무 사용자 권한 확대에 사용하지 않는다. -5. secret, raw bearer, database-access token, lookup key는 응답·로그·Git·Redmine에 기록하지 않는다. +3. 서비스 token은 Context attach 승인용일 뿐 사용자 권한을 결정하지 않는다. +4. 업무 권한 원천은 `CB_*` 테이블 하나이며 함수와 관리 UI 모두 이를 사용한다. +5. 예외·미매핑·권한 없음은 항상 거부다. +6. secret, raw bearer, database-access token, lookup key는 응답·로그·Git·Redmine에 기록하지 않는다. -## 9. 참고 +## 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) -- [Oracle: Prerequisites for Establishing a Local Security Context](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/prerequisites-establishing-local-security-context.html) -- [Oracle: End-User Security Context Issues](https://docs.oracle.com/en/database/oracle/oracle-database/26/ddscg/end-user-security-context-issues.html) diff --git a/docs/design/617-dds-mcp-end-user-context/fn-authenticate-mcp-bearer.md b/docs/design/617-dds-mcp-end-user-context/fn-authenticate-mcp-bearer.md index 5b5fc81..973bf81 100644 --- a/docs/design/617-dds-mcp-end-user-context/fn-authenticate-mcp-bearer.md +++ b/docs/design/617-dds-mcp-end-user-context/fn-authenticate-mcp-bearer.md @@ -4,7 +4,7 @@ ## 책임 -MCP Tool 요청의 opaque Bearer를 활성 `CB_APP_USER`로 해석하고 해당 사용자의 게시된 local DDS principal을 반환한다. OCI IAM client-credentials token을 업무 사용자 token으로 해석하지 않는다. +MCP Tool 요청의 opaque Bearer를 활성 `CB_APP_USER`로 해석하고 해당 사용자의 provisioning된 local DDS principal을 반환한다. OCI IAM client-credentials token을 업무 사용자 token으로 해석하지 않는다. ## 입력과 출력 @@ -14,10 +14,10 @@ MCP Tool 요청의 opaque Bearer를 활성 `CB_APP_USER`로 해석하고 해당 ## 규칙 1. `CB_AGENT_BEARER_KEY`의 해시 일치, 만료·회수 여부와 `CB_APP_USER.active`를 매 Tool 요청마다 확인한다. -2. `CB_DDS_END_USER_MAP`에서 `PUBLISHED` 상태의 사용자 매핑을 반드시 찾는다. +2. `CB_DDS_END_USER_MAP`에서 활성 local END USER provisioning 상태의 사용자 매핑을 반드시 찾는다. 3. 하나라도 없으면 `AUTHORIZATION_DENIED`로 끝내며 context attach 또는 보호 SQL을 호출하지 않는다. 4. raw bearer와 hash는 로그·MCP 응답에 노출하지 않는다. ## 주의 -현재 opaque Bearer의 사용자 매핑이 DDS 집행 주체를 결정한다. OCI IAM JWT의 사람 claim을 직접 사용하는 기능은 별도 OBO/authorization-code 확장이다. +현재 opaque Bearer의 사용자 매핑이 DDS 집행 주체를 결정한다. 역할·퍼미션은 이 함수에서 복제하지 않고 보호 SQL의 runtime authorization predicate가 `CB_*` 권한 테이블을 읽어 집행한다. OCI IAM JWT의 사람 claim을 직접 사용하는 기능은 별도 OBO/authorization-code 확장이다. diff --git a/docs/design/617-dds-mcp-end-user-context/fn-compile-dds-publish-plan.md b/docs/design/617-dds-mcp-end-user-context/fn-compile-dds-publish-plan.md index 168f2d4..9d5c22e 100644 --- a/docs/design/617-dds-mcp-end-user-context/fn-compile-dds-publish-plan.md +++ b/docs/design/617-dds-mcp-end-user-context/fn-compile-dds-publish-plan.md @@ -1,10 +1,10 @@ # 함수 명세: `compileDdsPublishPlan` (#617) -> **상태**: Draft · **분류**: 복잡 — 권한 컴파일·DDL diff·안전 검증 +> **상태**: Superseded by runtime authorization provisioning · 기존 사용자별 publish PoC 설명 ## 책임 -DDS 백오피스의 보호 객체, 접근 역할, 테이블 세부 권한, 사용자/그룹 할당을 실행 가능한 DDS DDL diff로 바꾼다. +DDS 백오피스의 보호 Object·CRUD·컬럼 그룹 메타데이터를 실행 가능한 장기 DDS DDL diff로 바꾼다. 사용자·그룹·역할·퍼미션 값은 런타임 권한 함수가 평가하므로 이 계획이 사용자별 Grant를 만들지 않는다. ## 시그니처 @@ -12,20 +12,18 @@ DDS 백오피스의 보호 객체, 접근 역할, 테이블 세부 권한, 사 ## 규칙 -1. 그룹을 포함해 역할별 유효 사용자를 계산한다. -2. `CB_APP_USER.userId`마다 ASCII `DDS_U_` END USER 이름을 결정한다. -3. 접근 역할마다 하나의 stable DATA ROLE 이름을 결정한다. -4. 사용자·역할·객체·세부 권한의 현재 게시 상태와 목표 상태를 비교한다. -5. 생성/교체/회수 DDL과 예상 영향 사용자·객체를 계산한다. -6. 행 조건 DSL은 whitelist 기반 compiler만 사용한다. raw SQL은 거부한다. -7. `DENY` 또는 지원하지 않는 권한 조합은 plan 생성 단계에서 오류로 끝낸다. +1. Object/CRUD/컬럼 그룹별 고정 DATA GRANT 이름과 컬럼 목록을 결정한다. +2. 각 Grant predicate가 `ORA_END_USER_CONTEXT.username`과 whitelist 상수를 `can_access`에 전달하도록 생성한다. +3. `DDS_MCP_RUNTIME` DATA ROLE과 local END USER provisioning 상태를 비교한다. +4. Object·컬럼·CRUD 메타데이터 변경만 DDL diff로 계산한다. +5. 사용자·그룹·역할·퍼미션 값 변경은 DDL diff에 포함하지 않는다. +6. raw SQL과 caller 제공 identifier는 거부한다. ## 출력 -- END USER 생성/활성화/비활성화 목록 -- DATA ROLE 생성/역할 부여/회수 목록 -- DATA GRANT 생성·교체·삭제 목록 -- 예상 영향 사용자·객체·컬럼 matrix +- 신규 END USER provisioning 및 공통 DATA ROLE 부여 목록 +- Object/CRUD/컬럼 그룹별 DATA GRANT 생성·교체·삭제 목록 +- 권한 함수/권한 테이블 prerequisite와 예상 영향 Object·컬럼 matrix - 이전/목표 fingerprint와 승인 대상 diff ## 실패 diff --git a/docs/design/617-dds-mcp-end-user-context/fn-evaluate-runtime-dds-permission.md b/docs/design/617-dds-mcp-end-user-context/fn-evaluate-runtime-dds-permission.md new file mode 100644 index 0000000..35c80cc --- /dev/null +++ b/docs/design/617-dds-mcp-end-user-context/fn-evaluate-runtime-dds-permission.md @@ -0,0 +1,53 @@ +# 함수 명세: `canAccess` (#617) + +> **상태**: Approved design · 구현 Pending · **분류**: 보안 핵심 + +## 책임 + +local DDS END USER Context와 기존 `CB_*` 권한 테이블을 사용해 Object·동작·컬럼 그룹·행 속성에 대한 접근을 판단한다. 권한 변경을 사용자별 DDS DDL 재게시 없이 다음 보호 SQL부터 반영한다. + +## 호출 형태 + +```sql +ADMIN.DDS_MCP_AUTHZ_PKG.can_access( + ORA_END_USER_CONTEXT.username, + 'ADMIN.CUSTOMER', + 'SELECT', + 'CONTACT', + tenant_id, + customer_type +) +``` + +반환값은 `1`(허용) 또는 `0`(거부)이다. 실제 함수 인자와 행 속성은 Object별 Grant가 사용하는 컬럼에 맞춰 제한한다. + +## 입력 신뢰 경계 + +| 입력 | 출처 | 신뢰 규칙 | +|---|---|---| +| END USER 이름 | `ORA_END_USER_CONTEXT.username` | Bearer 검증·Context attach 뒤 DB가 제공한 값만 사용 | +| Object·동작·컬럼 그룹 | DATA GRANT DDL의 문자열 상수 | whitelist 검증 후 provisioning된 값만 사용 | +| 테넌트·분류 등 행 속성 | 보호 Object의 현재 행 컬럼 | 함수 안에서 추가 SQL로 재조회하지 않음 | +| 사용자 ID·권한 값 | `CB_DDS_END_USER_MAP`, `CB_*` | 클라이언트/MCP 인자에서 받지 않음 | + +## 알고리즘 + +1. END USER 이름으로 `CB_DDS_END_USER_MAP`과 활성 `CB_APP_USER`를 찾는다. +2. 사용자 직접 역할과 활성 그룹의 역할을 합친다. +3. 대상 Object/동작/컬럼 그룹에 일치하는 `CB_PERMISSION`을 찾고 ALLOW와 DENY를 평가한다. +4. 권한 규칙과 전달된 행 속성으로 테넌트·부서·본인·상태·태그 범위를 평가한다. +5. 명시적 DENY, 매핑 없음, 비활성 사용자, 권한 없음, 검증 실패, 예외는 `0`을 반환한다. + +## DDS DATA GRANT 배치 규칙 + +- Object/CRUD/컬럼 그룹마다 장기 Grant를 하나 이상 만든다. +- `SELECT (column...)`과 `UPDATE (column...)`의 목록은 정적이며, 민감 컬럼마다 또는 승인 단위 컬럼 그룹마다 분리한다. +- Grant들은 additive이다. 공통 Role에 `AS SELECT` 또는 넓은 컬럼 목록을 만들지 않는다. +- predicate가 같은 보호 Object를 서브쿼리로 읽지 않게 하고, 권한 테이블만 읽는다. + +## 실패와 성능 + +- 패키지는 `AUTHID DEFINER`로 작성하며 권한 테이블 조회 권한은 직접 부여한다. +- 함수 오류는 로그에 안전한 식별자만 남기고 `0`을 반환한다. 오류를 MCP 응답에서 권한 존재 여부로 구분해 노출하지 않는다. +- `DETERMINISTIC`·임의 result cache는 사용하지 않는다. 권한 테이블 변경이 즉시 반영되어야 한다. +- 권한 테이블에는 END USER/user, role, Object, action, column group, active 상태를 시작 열로 하는 인덱스를 둔다. 대량 행 조회 대상은 `EXISTS`/조인 실행 계획을 별도 측정한다. diff --git a/docs/design/617-dds-mcp-end-user-context/fn-publish-and-verify-dds-plan.md b/docs/design/617-dds-mcp-end-user-context/fn-publish-and-verify-dds-plan.md index 68b8d99..ea69d6d 100644 --- a/docs/design/617-dds-mcp-end-user-context/fn-publish-and-verify-dds-plan.md +++ b/docs/design/617-dds-mcp-end-user-context/fn-publish-and-verify-dds-plan.md @@ -1,10 +1,10 @@ # 함수 명세: `publishDdsPlan` 및 `verifyDdsIsolation` (#617) -> **상태**: Draft · **분류**: 복잡 — 보안 DDL 변경·검증·드리프트 +> **상태**: Draft · **분류**: 보호 Object provisioning·검증·드리프트 ## 책임 -승인된 DDL plan을 멱등 실행하고, dictionary와 representative END USER matrix로 실제 결과를 확인한다. +승인된 Object/CRUD/컬럼 그룹 provisioning DDL을 멱등 실행하고, dictionary와 representative END USER matrix로 실제 결과를 확인한다. 권한 값 변경 자체는 이 함수를 호출하지 않는다. ## 시그니처 @@ -14,8 +14,8 @@ ## 게시 규칙 1. 승인 사유와 diff fingerprint가 없으면 게시하지 않는다. -2. Publisher lock으로 같은 보호 객체의 동시 게시를 막는다. -3. END USER/Data Role/Data Grant를 dependency 순서로 적용한다. +2. Publisher lock으로 같은 보호 객체의 동시 provisioning을 막는다. +3. 공통 Data Role, 신규 END USER, 함수 prerequisite, Data Grant를 dependency 순서로 적용한다. 4. 각 DDL 결과와 Oracle 오류를 `DdsPublishRun`에 기록한다. 5. 중간 실패 시 성공으로 표시하지 않는다. 부분 적용 상태로 기록하고 drift로 승격한다. @@ -23,7 +23,7 @@ 1. `DBA_END_USERS`, `DBA_DATA_ROLES`, `DBA_DATA_ROLE_GRANTS`, `DBA_DATA_GRANTS`를 목표 fingerprint와 비교한다. 2. 대표 사용자별 Context, 객체 접근, 행, 컬럼 결과를 검증한다. -3. 역할 제거와 객체 미권한 경로도 반드시 검증한다. +3. 권한 테이블의 ALLOW/DENY·역할 변경이 DDL 없이 다음 SQL에 반영되는지 검증한다. 4. Context attach/clear 격리 검증이 실패하면 publish 결과를 정상으로 표시하지 않는다. ## 실패 diff --git a/docs/design/617-dds-mcp-end-user-context/fn-with-dds-end-user-context.md b/docs/design/617-dds-mcp-end-user-context/fn-with-dds-end-user-context.md index cca201f..135004d 100644 --- a/docs/design/617-dds-mcp-end-user-context/fn-with-dds-end-user-context.md +++ b/docs/design/617-dds-mcp-end-user-context/fn-with-dds-end-user-context.md @@ -17,7 +17,7 @@ ## 불변식 - local username+lookup-key Context에는 `withDataRoles(...)`를 전달하지 않는다. -- local END USER에 미리 `GRANT DATA ROLE`된 역할과 `DATA GRANT`만 적용한다. +- local END USER에 미리 `GRANT DATA ROLE`된 공통 runtime 역할과 Object별 `DATA GRANT`만 적용한다. 사용자별 권한 판단은 Data Grant predicate가 권한 테이블에서 수행한다. - 연결 풀 재사용 시에도 이전 사용자 Context가 남지 않아야 한다. ## 실증