본문으로 건너뛰기

쿼리 실행 DataGrant 시행 (#2562)

SQL 에디터/노트북 프록시의 쿼리 실행 평면(POST /api/v1/query/execute)에서 테이블 SELECT 권한(DataGrant)을 검사하는 기능의 운영 가이드입니다. ABAC(행 필터·컬럼 마스크)와 별개로 동작하며, 관측은 항상 켜져 있고 차단만 단계적으로 확대합니다.

모드 (env)

env동작
GEND_GRANT_ENFORCEMENT_MODEoff관측만 — 위반을 카운터/로그로 기록, 차단 없음 (Phase 1)
whitelistGEND_GRANT_ENFORCED_SCHEMAScatalog.schema 만 403 차단 (Phase 2, 현재 prod)
all전 스키마 deny-by-default (Phase 3)
GEND_GRANT_ENFORCED_SCHEMASCSV예: 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/snapshotsviewer+대상 테이블 SELECT
POST /api/v1/time-travel/queryanalyst+대상 테이블 SELECT
GET /api/v1/lineage/{catalog}/{schema}/{table}viewer+기준 테이블 SELECT
GET /api/v1/lineage/{…}/impactviewer+기준 테이블 SELECT
GET /api/v1/lineage/tablesviewer+보유 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 오탐을 피하려 관측하지 않습니다.

운영 런북 — 단계 전환

  1. 관측 (2주 권장): Grafana 에서 gend_query_grant_violation_total 을 스키마별로 확인해 상위 위반 스키마·규모를 파악합니다.

  2. 백필 (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-run
    kubectl 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_grant UNIQUE 가 workspace_id 를 포함하지 않아, 멀티 워크스페이스 사용자는 동일 (catalog, schema) 좌표에 한 워크스페이스의 grant 만 저장됩니다 (스크립트가 충돌을 감지해 warning 으로 스킵). 해당 사용자는 관리자 화면에서 role/group 레벨 grant 로 보완하세요.

  3. 화이트리스트 확대: GEND_GRANT_ENFORCED_SCHEMAS 에 스키마를 추가하고 configmap 적용 + kubectl rollout restart deploy/gend-api -n gend.

  4. 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 로 추출. 파싱 실패 시 정규식으로 폴백 — 배포만으로 막히지 않는다
astAST 로 추출. 파싱 실패 시 차단(판정 불가 = 차단)
off기존 정규식만 — 롤백용

알 수 없는 값은 ast 로 폴백한다(오타가 전면 롤백으로 번지지 않도록).

:::note 추출 자체는 모드와 무관하게 AST 를 먼저 쓴다 observe파싱에 실패했을 때만 정규식으로 떨어진다. 즉 정규식이 놓치던 41% 는 기본 모드에서도 즉시 회수된다. 모드가 결정하는 것은 "파싱 실패를 어떻게 다룰까"뿐이다. :::

ast 로 전환하는 절차

  1. observe 로 배포
  2. gend_sql_guard_parse_failures_total{mode="observe"}최소 1주 관측
  3. 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 (기본)위반을 카운터·로그로만 남기고 통과 — 배포만으로 아무것도 멈추지 않는다
enforce403
off검사 안 함 (롤백)

알 수 없는 값은 enforce 로 폴백한다.

enforce 로 전환하는 절차

  1. observe 로 배포
  2. gend_mart_source_authz_violations_total 을 최소 1주 관측
  3. actor="no_owner" 가 오르면 → 그 마트의 소유자를 지정하거나 비활성화
  4. actor="requester"/"owner" 가 오르면 → 권한을 부여하거나 마트를 정리
  5. 전부 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) 입니다 — iceberghive 가 같은 HMS·같은 S3 경로를 가리키므로, 카탈로그 이름으로 키를 잡으면 같은 실체가 두 개로 등록됩니다.

:::tip 바인딩을 실제로 만들고 거두는 절차 개별 발급·승격·회수는 관리자 API 로 합니다 — 네임스페이스 바인딩 발급·회수 런북 (#3071). 아래 배치 스크립트는 증거에서 소급 발급하는 도구라, 근거 테이블에 기록이 없는 경로(공유 네임스페이스 datasets 가 그 경우)는 파생되지 않습니다. :::

모드

GEND_NAMESPACE_BINDING_MODE 로 단계 시행한다.

동작
off게이트 미실행
observe (기본)판정만 기록하고 통과
enforceunbound 참조를 차단 (현재 prod)

알 수 없는 값은 observe 로 폴백한다 — 이 게이트는 다른 단계 시행과 달리 분포를 관측한 적 없는 상태로 배포됐기 때문에, 안전한 쪽이 "차단"이 아니라 "관측"이다.

판정 두 종류 — 차단되는 것은 하나뿐

판정enforce 에서
unbound네임스페이스는 레지스트리에 있는데 이 워크스페이스에 바인딩이 없다차단
unknown_ns네임스페이스가 레지스트리에 없다차단하지 않음

:::danger unknown_ns 를 차단하면 안 된다 차단하면 가용성이 레지스트리 완전성에 종속됩니다. 새 스키마를 만들 때마다 census 를 돌리기 전까지 정상 질의가 거부됩니다. 실측으로도 prod 질의 2,797건 중 49건이 이 경로였습니다 — 제외하지 않고 켰다면 그대로 깨졌습니다 (#2838). :::

파싱 실패 시에도 차단하지 않는다(참조를 모르므로 판정 불가). 읽기 경로에서는 SqlGuard 가 파싱 불가 SQL 을 먼저 거부한다.

enforce 로 전환하는 절차

  1. observe 로 배포하고 바인딩을 발급한다 (issue_namespace_bindings.py — grant + 카탈로그 scope grant + 쿼리 이력). 쿼리 이력까지 보는 이유는 grant 없이 실사용 중인 경로를 놓치지 않기 위해서다.
  2. query_history 를 전수 재생해 unbound 건수를 센다. 0 이 아니면 그 워크스페이스에 바인딩이 빠진 것이므로 먼저 발급한다.
  3. unknown_ns 제외 코드가 배포됐는지 확인한다 (선행 조건).
  4. configmap 을 enforce 로 바꾸고 rollout restart.
  5. 판별 쌍으로 확인한다 — 같은 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 로 바꾸고 재시작한다.

조회 방법은 아래 차단된 주체 조회 절과 동일하다 — eventNAMESPACE_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_MODEoff | 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 증거만 봅니다. iceberg.datasets신규 워크스페이스마다 수동 발급이 필요v1.2.1+ 에서 해소됐습니다. 데이터셋이 워크스페이스 스키마로 옮겨가(ADR-0044) 프로비저너가 만드는 owner 바인딩에 포함되므로, 별도 수동 발급이 필요 없습니다. 2026-08-05 장애의 원인이던 "공유 네임스페이스 발급 누락" 경로 자체가 사라졌습니다. 절차 참고는 발급·회수 런북. :::

근거는 실쓰기 증거(data_marts 의 활성 마트 ws×타겟 — 오염 방지를 위해 dry-run 이 증거의 최근 생성 시각을 출력하므로 실행 전 확인)입니다. CSV 승격· 학습 데이터셋 등 과거 기록이 없는 경로는 여기서 파생하지 않습니다 — observe 분포(reason="unbound")에 잡히면 그때 해당 쌍을 발급합니다. 바인딩은 (namespace, workspace) 당 1행이라 기존 read 바인딩은 write 로 승격됩니다 — 읽기 게이트는 모드 무관 바인딩을 인정하므로 승격이 읽기를 깨지 않습니다.

enforce 전환 절차

  1. --write 발급 (dry-run 먼저)
  2. 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 승격·학습 데이터셋 생성)가 관측창 안에 실제로 지나갔는지 감사 로그로 교차 확인한 뒤 판정하십시오.
  3. configmap GEND_WRITE_NAMESPACE_MODE: "enforce" + 재시작
  4. 릴리스 노트에 "기존 클라이언트의 임의 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 로 수동 발급하십시오