쿼리 실행 DataGrant 시행 (#2562)
SQL 에디터/노트북 프록시의 쿼리 실행 평면(POST /api/v1/query/execute)에서 테이블
SELECT 권한(DataGrant)을 검사하는 기능의 운영 가이드입니다. ABAC(행 필터·컬럼
마스크)와 별개로 동작하며, 관측은 항상 켜져 있고 차단만 단계적으로 확대합니다.
모드 (env)
| env | 값 | 동작 |
|---|---|---|
GEND_GRANT_ENFORCEMENT_MODE | off | 관측만 — 위반을 카운터/로그로 기록, 차단 없음 (Phase 1) |
whitelist | GEND_GRANT_ENFORCED_SCHEMAS 의 catalog.schema 만 403 차단 (Phase 2, 현재 prod) | |
all | 전 스키마 deny-by-default (Phase 3) | |
GEND_GRANT_ENFORCED_SCHEMAS | CSV | 예: iceberg.datasets,iceberg.intel_silver |
:::info 학습 데이터셋은 이 CSV 와 무관하게 항상 시행됩니다 (v1.2.1+)
데이터셋이 워크스페이스 스키마(iceberg.ws_<슬러그>)로 옮겨가면서(ADR-0044), CSV 를
스키마명으로 관리하면 워크스페이스가 늘 때마다 갱신해야 하고 빠뜨리면 그 워크스페이스의
데이터셋이 조용히 시행 범위 밖으로 나갑니다. 그래서 데이터셋 테이블은 스키마가 아니라
테이블 접두어(ds_) 로 식별해 whitelist 모드에서도 항상 검사합니다.
CSV 에 iceberg.datasets 가 남아 있어도 무해합니다 — 이관 기간 동안 양쪽을 함께 덮습니다.
:::
- 관측 검사 자체는 fail-open(검사 실패가 쿼리를 깨지 않음).
- 관측 카운터:
gend_query_grant_violation_total{catalog,schema}/ 차단 카운터:gend_query_grant_denied_total{catalog,schema}/ 감사 로그 태그:GRANT_OBSERVE.
:::warning admin 우회는 더 이상 무조건이 아니다 (#2797 D2)
예전에는 admin 이 이 시행을 항상 우회했습니다. 2026-08-03 부터 prod 는
GEND_ADMIN_DATA_PLANE_MODE=enforce 이며, 데이터평면(카탈로그 목록·행·쿼리
결과) 우회에는 ws_break_glass 역할이 추가로 필요합니다. 워크스페이스·grant CRUD
와 감사 조회 같은 제어평면은 종전대로 admin 만으로 가능합니다.
자세한 내용은 역할·권한 을 보십시오.
:::
직접 지정 테이블 엔드포인트
모드 스위치와 무관하게 항상 deny-by-default 인 경로가 있습니다 — 테이블이 SQL 문자열이 아니라 경로 파라미터/요청 본문으로 직접 지정되는 엔드포인트입니다.
| 엔드포인트 | 역할 | grant |
|---|---|---|
GET /api/v1/time-travel/snapshots | viewer+ | 대상 테이블 SELECT |
POST /api/v1/time-travel/query | analyst+ | 대상 테이블 SELECT |
GET /api/v1/lineage/{catalog}/{schema}/{table} | viewer+ | 기준 테이블 SELECT |
GET /api/v1/lineage/{…}/impact | viewer+ | 기준 테이블 SELECT |
GET /api/v1/lineage/tables | viewer+ | 보유 grant 로 목록 필터 |
판정은 services/access/read_policy.check_table_select() 가 단독 소유합니다
(admin 예외는 select_enforced 와 동일). 왜 모드를 보지 않는가:
- SQL 파싱 실패로 인한 fail-open 이 없다 — 테이블이 이미 구조화되어 들어온다.
- 단계적으로 보존할 레거시 동작이 없다 — 2026-07-30 까지 이 경로들엔 인가가
전혀 없었다(핸들러에
user파라미터 자체가 없었다). 모드에 걸었다면 당시 prod 값(whitelist+ 대상 테이블 0개)에서 아무것도 막지 못한 채 통과했을 것이다.
추가로 POST /time-travel/query 는 항상 SELECT * 를 생성하므로 컬럼 마스킹이
구조적으로 적용될 수 없습니다(마스커는 이름이 적힌 컬럼만 치환). 마스크 정책이
있는 테이블은 원본을 흘리는 대신 403 으로 거부하고, SQL 에디터에서 컬럼을
명시해 조회하도록 안내합니다. 행 필터(row_filter)는 서브쿼리 래핑으로 정상 적용됩니다.
커버리지와 한계
- 커버: SQL 에디터·노트북 프록시 (동일 execute 경로). MCP·Agent·CLI 경로는 #2701
부터 같은 시행 헬퍼(
services/access/read_policy)를 타므로 동일한 관측·차단이 적용됩니다 — 그 이전에는 SDK 계층에 인가가 전혀 없었습니다. 운영은 SDK·MCP 데이터 인가 참조. - 미커버: Kyuubi(Spark JDBC) 직결 — API 를 거치지 않아 본 시행 밖입니다. 전면 통제가 필요하면 Trino system access control(OPA/file-based) 도입을 별도 결정하세요.
- 테이블 참조 인식은 3-part(
catalog.schema.table) + 기본 catalog 로 보정한 2-part 까지입니다. 1-part(비정규화) 참조는 CTE 오탐을 피하려 관측하지 않습니다.
운영 런북 — 단계 전환
-
관측 (2주 권장): Grafana 에서
gend_query_grant_violation_total을 스키마별로 확인해 상위 위반 스키마·규모를 파악합니다. -
백필 (dry-run → apply): 실사용 기반 grant 를 선발급합니다.
kubectl exec -n gend deploy/gend-api -c gend-api -- \python3 -m gend_api.scripts.backfill_query_grants --days 30 # dry-runkubectl exec -n gend deploy/gend-api -c gend-api -- \python3 -m gend_api.scripts.backfill_query_grants --days 30 --apply최근 N 일 성공 쿼리의 (사용자, catalog, schema) 실사용을 집계해 스키마 레벨 SELECT grant 를 발급합니다 (기보유 grant 는 스킵).
알려진 한계:
uq_data_grantUNIQUE 가 workspace_id 를 포함하지 않아, 멀티 워크스페이스 사용자는 동일 (catalog, schema) 좌표에 한 워크스페이스의 grant 만 저장됩니다 (스크립트가 충돌을 감지해 warning 으로 스킵). 해당 사용자는 관리자 화면에서 role/group 레벨 grant 로 보완하세요. -
화이트리스트 확대:
GEND_GRANT_ENFORCED_SCHEMAS에 스키마를 추가하고 configmap 적용 +kubectl rollout restart deploy/gend-api -n gend. -
deny-by-default 전환: 백필 후 위반 카운터가 0 에 수렴하면
GEND_GRANT_ENFORCEMENT_MODE: "all"로 전환합니다. 문제가 생기면 env 롤백만으로 즉시 복구됩니다 (코드 배포 불요).
사용자 안내
차단 시 사용자는 SELECT 권한이 없는 테이블입니다: <목록> 403 을 받습니다 —
관리자 화면(접근 정책/Grant)에서 해당 사용자의 role/group/user grant 를 발급하세요.
SQL 테이블 추출 — 정규식에서 AST 로 (#2716)
행 필터·컬럼 마스크·권한 관측은 모두 SQL 에서 추출한 테이블 목록 위에 올라간다. 그 추출이 놓치면 그 쿼리에는 아무 정책도 적용되지 않는다.
왜 바꿨는가
이전 추출기는 정규식 (?:FROM|JOIN)\s+(\w+)\.(\w+)\.(\w+) 하나였다. \w 는 따옴표를
매칭하지 못하므로 FROM "iceberg"."gold"."salaries" 가 빈 목록을 돌려줬고,
정책 엔진은 추출이 비면 원본 SQL 을 그대로 반환한다.
prod 쿼리 이력 실측(2026-07-31):
| 항목 | 결과 |
|---|---|
| 성공한 쿼리 | 120건 |
| 정규식이 테이블을 놓친 쿼리 | 49건 (41%) |
| AST 가 놓친 쿼리 | 0건 |
모드
GEND_SQL_GUARD_EXTRACT_MODE 로 단계 시행한다.
| 값 | 동작 |
|---|---|
observe (기본) | AST 로 추출. 파싱 실패 시 정규식으로 폴백 — 배포만으로 막히지 않는다 |
ast | AST 로 추출. 파싱 실패 시 차단(판정 불가 = 차단) |
off | 기존 정규식만 — 롤백용 |
알 수 없는 값은 ast 로 폴백한다(오타가 전면 롤백으로 번지지 않도록).
:::note 추출 자체는 모드와 무관하게 AST 를 먼저 쓴다
observe 는 파싱에 실패했을 때만 정규식으로 떨어진다. 즉 정규식이 놓치던 41% 는
기본 모드에서도 즉시 회수된다. 모드가 결정하는 것은 "파싱 실패를 어떻게 다룰까"뿐이다.
:::
ast 로 전환하는 절차
observe로 배포gend_sql_guard_parse_failures_total{mode="observe"}를 최소 1주 관측- 0 이면
GEND_SQL_GUARD_EXTRACT_MODE=ast로 전환
:::warning 코퍼스 표본만으로 곧바로 차단하지 말 것
성공 쿼리 120건이 전부 파싱됐지만, 구현 중 코퍼스에 없던 유효한 Trino 문법을
발견했다 — WITH x AS (TABLE cat.sch.tbl) 의 TABLE 관계 구문을 파서가 처리하지
못한다. 이건 모든 사용자의 읽기 경로이므로 관측을 거쳐 전환한다.
:::
알람
increase(gend_sql_guard_parse_failures_total{mode="ast"}[10m]) > 0
ast 모드에서 이 값이 오르면 정상 쿼리가 차단되고 있을 수 있다. 로그의
SQL_PARSE_FAIL 항목으로 어떤 SQL 인지 확인하고, 파서 한계면 observe 로 되돌린다.
대소문자
추출은 원문의 대소문자를 보존한다. 하위 정책 대조가 그 값으로 data_grants 행과
매칭하기 때문이다 — prod 에 catalog_name='tpcH' 인 grant 가 실재하므로, 소문자로
정규화하면 동작 중인 권한이 매칭되지 않는다.
데이터마트 원천 테이블 인가 (#2717)
데이터마트는 원천 SQL 을 실행해 별개 테이블을 만든다. 산출물에는 원천의 행 필터· 컬럼 마스크가 따라가지 않으므로, 원천 인가가 없으면 권한 우회가 영구화된다.
무엇을 검사하는가
| 시점 | 검사 대상 신원 | 이유 |
|---|---|---|
| 생성 | 요청자 | 그 사람이 만든다 |
| 갱신 | 소유자(created_by) | 갱신 API 는 관리자 전용이라 요청자로 보면 항상 통과한다 |
:::warning 갱신에서 요청자를 보면 안 되는 이유
POST /data-marts/{id}/refresh 는 관리자 전용이고, 관리자는 가시성 검사를 우회한다.
요청자로 재평가하면 항상 통과해서 "생성 후 권한이 회수된 경우"를 전혀 잡지 못한다.
:::
모드
GEND_DATA_MART_SOURCE_AUTHZ_MODE 로 단계 시행한다.
| 값 | 동작 |
|---|---|
observe (기본) | 위반을 카운터·로그로만 남기고 통과 — 배포만으로 아무것도 멈추지 않는다 |
enforce | 403 |
off | 검사 안 함 (롤백) |
알 수 없는 값은 enforce 로 폴백한다.
enforce 로 전환하는 절차
observe로 배포gend_mart_source_authz_violations_total을 최소 1주 관측actor="no_owner"가 오르면 → 그 마트의 소유자를 지정하거나 비활성화actor="requester"/"owner"가 오르면 → 권한을 부여하거나 마트를 정리- 전부 0 이면
enforce로 전환
:::caution 소유자 기록이 없는 마트
운영 환경에는 소유자(created_by)가 비어 있는 마트가 있을 수 있다(초기 시드 등).
그 마트는 갱신 시 재평가할 신원이 없어 actor="no_owner" 로 관측된다.
enforce 전환 전에 반드시 해소해야 그 마트의 갱신이 멈추지 않는다.
:::
한정자 없는 참조
select * from mart1 처럼 카탈로그·스키마가 없는 참조도 인가 대상이다. 마트
산출물(target_table)의 카탈로그를 기준으로 정규화하며, 정규화 기준이 없으면
판정 불가로 보아 위반 처리한다.
알람
increase(gend_mart_source_authz_violations_total{mode="enforce"}[1h]) > 0
네임스페이스 바인딩 게이트 (#2797 PR-12)
grant 는 "누가 무엇을 볼 수 있나" 를 봅니다. 바인딩 게이트는 그 앞에서 "이 워크스페이스가 이 네임스페이스를 쓸 자격이 있나" 를 봅니다. grant 가 실수로 넓게 발급돼도 워크스페이스 경계를 넘지 못하게 하는 두 번째 층입니다.
네임스페이스 키는 (catalog, schema) 가 아니라 (metastore, database) 입니다 —
iceberg 와 hive 가 같은 HMS·같은 S3 경로를 가리키므로, 카탈로그 이름으로 키를
잡으면 같은 실체가 두 개로 등록됩니다.
:::tip 바인딩을 실제로 만들고 거두는 절차
개별 발급·승격·회수는 관리자 API 로 합니다 —
네임스페이스 바인딩 발급·회수 런북 (#3071).
아래 배치 스크립트는 증거에서 소급 발급하는 도구라, 근거 테이블에 기록이 없는
경로(공유 네임스페이스 datasets 가 그 경우)는 파생되지 않습니다.
:::
모드
GEND_NAMESPACE_BINDING_MODE 로 단계 시행한다.
| 값 | 동작 |
|---|---|
off | 게이트 미실행 |
observe (기본) | 판정만 기록하고 통과 |
enforce | unbound 참조를 차단 (현재 prod) |
알 수 없는 값은 observe 로 폴백한다 — 이 게이트는 다른 단계 시행과 달리
분포를 관측한 적 없는 상태로 배포됐기 때문에, 안전한 쪽이 "차단"이 아니라
"관측"이다.
판정 두 종류 — 차단되는 것은 하나뿐
| 판정 | 뜻 | enforce 에서 |
|---|---|---|
unbound | 네임스페이스는 레지스트리에 있는데 이 워크스페이스에 바인딩이 없다 | 차단 |
unknown_ns | 네임스페이스가 레지스트리에 없다 | 차단하지 않음 |
:::danger unknown_ns 를 차단하면 안 된다
차단하면 가용성이 레지스트리 완전성에 종속됩니다. 새 스키마를 만들 때마다
census 를 돌리기 전까지 정상 질의가 거부됩니다. 실측으로도 prod 질의 2,797건 중
49건이 이 경로였습니다 — 제외하지 않고 켰다면 그대로 깨졌습니다 (#2838).
:::
파싱 실패 시에도 차단하지 않는다(참조를 모르므로 판정 불가). 읽기 경로에서는 SqlGuard 가 파싱 불가 SQL 을 먼저 거부한다.
enforce 로 전환하는 절차
observe로 배포하고 바인딩을 발급한다 (issue_namespace_bindings.py— grant + 카탈로그 scope grant + 쿼리 이력). 쿼리 이력까지 보는 이유는 grant 없이 실사용 중인 경로를 놓치지 않기 위해서다.query_history를 전수 재생해unbound건수를 센다. 0 이 아니면 그 워크스페이스에 바인딩이 빠진 것이므로 먼저 발급한다.unknown_ns제외 코드가 배포됐는지 확인한다 (선행 조건).- configmap 을
enforce로 바꾸고rollout restart. - 판별 쌍으로 확인한다 — 같은 SQL 을 바인딩된 워크스페이스와 아닌 워크스페이스로 각각 실행해 후자만 차단되는지 본다.
2026-08-03 prod 전환 시 실측: 통과 2,696 / unknown_ns 49 / 파싱실패 31 /
참조없음 21 / unbound 0 — 실트래픽 무영향.
알람
increase(gend_namespace_binding_gap_total{mode="enforce",reason="unbound"}[1h]) > 0
enforce 에서 이 값이 오르면 정상 사용자가 차단되고 있을 수 있다. 감사 로그의
NAMESPACE_BINDING_GAP 으로 어느 워크스페이스·네임스페이스인지 확인하고, 정당한
사용이면 바인딩을 발급한다. 되돌리려면 observe 로 바꾸고 재시작한다.
조회 방법은 아래 차단된 주체 조회 절과 동일하다 —
event 에 NAMESPACE_BINDING_GAP 이 함께 들어간다.
:::note 메트릭 라벨에 스키마·테이블 이름을 넣지 않는다 그 이름 자체가 테넌트 사업 정보이며, 이 게이트가 막으려는 정보를 메트릭으로 다시 노출하게 됩니다. :::
쓰기 타겟 네임스페이스 게이트 (#2798 PR-15)
읽기 바인딩 게이트가 "어느 네임스페이스를 읽을 수 있나"를 본다면, 이 게이트는 "어느 네임스페이스에 자산을 만들 수 있나"를 봅니다. 대상 경로:
| 경로 | 게이트 지점 |
|---|---|
| 데이터 마트 생성·타겟 변경 | data_mart.py create / update |
CSV 승격 (commit) | csv_ingest_service.py |
| 학습 데이터셋 생성 | training_dataset.py (타겟 = 워크스페이스 physical_schema — 고정값 아님) |
제외(사유 문서화): 문서 수집(ingestion_service)은 쓰기 타겟이 Trino 가 아니라
Weaviate/PG 라 벡터 평면 격리(D7, #2720) 소관. Pipeline Studio 의 Trino 쓰기는
intel-sink 하드코딩 자산(platform 소유, 스펙 명시 제외)뿐입니다.
모드
GEND_WRITE_NAMESPACE_MODE — off | observe (기본) | enforce.
알 수 없는 값은 observe 폴백(읽기 게이트와 같은 이유).
판정: 타겟 (catalog, schema) 의 물리 네임스페이스에 active workspace 의
access_mode='write' 바인딩이 있어야 합니다. read 바인딩은 쓰기 자격이
아닙니다.
판정 (reason) | 뜻 | enforce 에서 |
|---|---|---|
unbound | 네임스페이스는 아는데 write 바인딩 없음 | 차단 — 바인딩 발급으로 해소 |
no_workspace | 호출자의 활성 워크스페이스를 해석할 수 없음 | 차단 — 쓰기(자산 생성)는 워크스페이스 컨텍스트 필수 |
unknown_ns | 레지스트리에 없는 네임스페이스 | 차단하지 않음 (읽기 게이트와 같은 가용성 원칙) |
:::warning no_workspace 분포가 선행 조건을 알려준다
실측(2026-08-03)상 인증 트래픽의 99% 가 활성 워크스페이스 없이(tenantless)
들어옵니다. tenantless 는 헤더 누락이 아니라 "멤버십 0 인 주체" 입니다 —
X-Workspace-Slug 헤더가 없어도 서버가 JWT 멤버십 첫 항목으로 폴백하므로,
멤버십이 하나라도 있는 사용자는 tenantless 가 되지 않습니다. 남는 주체는
① 멤버십 없는 admin(폴링 계정 포함 — 대부분), ② 워크스페이스 미할당 사용자입니다.
해소는 사유별로 다릅니다: admin 은 스위처의 "전체 워크스페이스 (관리자)"
섹션에서 활성 워크스페이스를 명시 선택(impersonation — 접근이 감사에 기록됨)하고,
미할당 사용자는 워크스페이스 멤버십을 부여합니다. observe 의 no_workspace 가
0 이 아닌 채 enforce 로 켜면 이들의 쓰기가 403 이 됩니다.
:::
write 바인딩 발급
python -m gend_api.scripts.issue_namespace_bindings --write # dry-run
python -m gend_api.scripts.issue_namespace_bindings --write --execute
:::warning 공유 네임스페이스 datasets 는 여기서 나오지 않습니다
프로비저너는 자기 소유 스키마 2개만, 이 스크립트는 data_marts 증거만 봅니다.
— v1.2.1+ 에서
해소됐습니다. 데이터셋이 워크스페이스 스키마로 옮겨가(ADR-0044) 프로비저너가 만드는
owner 바인딩에 포함되므로, 별도 수동 발급이 필요 없습니다. 2026-08-05 장애의 원인이던
"공유 네임스페이스 발급 누락" 경로 자체가 사라졌습니다. 절차 참고는
발급·회수 런북.
:::iceberg.datasets 는 신규 워크스페이스마다 수동 발급이 필요
근거는 실쓰기 증거(data_marts 의 활성 마트 ws×타겟 — 오염 방지를 위해
dry-run 이 증거의 최근 생성 시각을 출력하므로 실행 전 확인)입니다. CSV 승격·
학습 데이터셋 등 과거 기록이 없는 경로는 여기서 파생하지 않습니다 — observe
분포(reason="unbound")에 잡히면 그때 해당 쌍을 발급합니다. 바인딩은
(namespace, workspace) 당 1행이라 기존 read 바인딩은 write 로 승격됩니다 —
읽기 게이트는 모드 무관 바인딩을 인정하므로 승격이 읽기를 깨지 않습니다.
enforce 전환 절차
--write발급 (dry-run 먼저)observe분포 확인 —gend_write_namespace_gap_total{mode="observe"}에서reason="unbound"와reason="no_workspace"둘 다 0 이어야 전환합니다. enforce 는 두 사유 모두 403 이므로unbound만 보고 켜면 tenantless 쓰기가 그대로 차단됩니다 (unbound는 바인딩 발급으로,no_workspace는 admin 스위처 명시 선택 + 멤버십 부여로 해소 — 위 경고 참조). (2026-08-04 계측: 발급 전엔 기존 바인딩 46건 전부 read 라 100% 거부 상태였다) ★ 0 은 "쓰기 트래픽을 관측하고도 0" 이어야 의미가 있습니다 — 카운터는 파드 재시작 시 리셋되므로, 실쓰기(DataMart 생성·CSV 승격·학습 데이터셋 생성)가 관측창 안에 실제로 지나갔는지 감사 로그로 교차 확인한 뒤 판정하십시오.- configmap
GEND_WRITE_NAMESPACE_MODE: "enforce"+ 재시작 - 릴리스 노트에 "기존 클라이언트의 임의
target_schema거부" 명시 (스펙 요구)
알람
increase(gend_write_namespace_gap_total{mode="enforce",reason=~"unbound|no_workspace"}[1h]) > 0
enforce 가 403 하는 두 사유(unbound·no_workspace)를 모두 감시합니다 —
unbound 만 보면 tenantless 차단이 무수신이 됩니다. 되돌리려면 observe(또는
off) + 재시작.
차단된 주체 조회 (v1.1+)
알람은 사유별 건수만 알려줍니다. 누가 어느 네임스페이스에서 막혔는지는 감사 인덱스에서 찾습니다 — 메트릭 라벨에는 이름을 넣지 않기 때문입니다(아래 note).
OS=opensearch-… # kubectl -n gend get pods | grep '^opensearch-'
kubectl -n gend exec "$OS" -c opensearch -- curl -sk \
--cert /usr/share/opensearch/config/transport/tls.crt \
--key /usr/share/opensearch/config/transport/tls.key \
-H 'Content-Type: application/json' \
'https://localhost:9200/gend-audit-*/_search' -d '{
"size": 20,
"query": {"bool": {"filter": [
{"terms": {"event": ["WRITE_NAMESPACE_GAP", "NAMESPACE_BINDING_GAP"]}},
{"term": {"mode": "enforce"}}
]}},
"sort": [{"@timestamp": "desc"}],
"_source": ["@timestamp","event","reason","user_id","catalog","schema"]
}'
:::warning admin basic auth 로는 조회할 수 없습니다
보안 플러그인 제약으로 admin 계정은 인덱스 조회 권한이 없습니다(#899).
위처럼 transport 클라이언트 인증서로 호출하세요.
:::
:::danger 이 레코드는 2026-08-07 이전에는 존재하지 않았습니다
그 전까지 게이트는 모듈 로거로 평문만 남겼고, fluent-bit 은
log gend\.audit + event . 두 조건으로 거르므로 감사 인덱스에 한 건도
적재되지 않았습니다. 파드는 배포마다 교체되어 kubectl logs 로도 몇 시간이면
사라집니다 — 알람이 가리키는 증거가 실제로는 없었습니다 (#3060).
새 게이트·시행 경로를 추가할 때는 logging.getLogger("gend.audit") 로
event 키를 포함한 JSON 한 줄을 내야 합니다. 모듈 로거로 내면 조용히
소멸합니다.
:::
쓰기 직후 창작자 grant 자동 발급 (#2798 PR-16)
자산을 만든 쓰기 경로(DataMart 생성·타겟 변경, CSV 승격, 학습 데이터셋 생성)가
성공 확정되면 창작자에게 table-scope ALL grant 를 자동 발급합니다. 종전엔
학습 데이터셋 한 곳만 발급했고 그마저 workspace_id 가 NULL 이라 기본
워크스페이스 밖에서는 무효였습니다 — grant 시행(whitelist/all)을 켜면
"만든 사람조차 자기 자산을 조회할 수 없는" 상태가 됩니다.
동작 원칙:
- grantee 는
user_id(Keycloak sub) · table-scope 만 (스키마 레벨 발급 금지) workspace_id는 자산 행과 같은 워크스페이스로 명시 스탬프- 발급 실패는 쓰기를 깨뜨리지 않습니다 — savepoint 로 격리되고 결과만 메트릭에 남습니다. 시스템/HMAC 내부 경로(user 없음)는 발급하지 않습니다
관측:
sum by (path, outcome) (increase(gend_creator_grant_total[24h]))
issued가 실쓰기와 함께 올라야 정상 — 실쓰기가 있는데issued=0 이면 배선 회귀를 의심하십시오 (0 은 "문제없음"과 "배선 끊김"을 구분하지 못합니다)conflict는 동일 좌표의 타 워크스페이스 grant 가 UNIQUE(uq_data_grant가 workspace_id 를 포함하지 않는 PR-17 전 제약)를 선점한 경우 — 발급이 생략된 것이므로 필요 시POST /api/v1/data-grants로 수동 발급하십시오