Skip to content

명시적 신원 전달 전면 적용 — 계정인 척·계정 전환 없는 데이터베이스 (2026-09-01)

  • 기간: 2026-08-31 ~ 2026-09-01 (1세션 연속, 오너: "명시적 신원 전달 원칙을 앱 전체에 적용… 비용을 고려하지 말고 확실하고 세세하게" + "중간에 묻지 말고 phase 5까지 완주")
  • 랜딩: PR #1035(묶음 A: 20260901800000) · PR #1037(묶음 B+C: 20260901830000·840000·850000) — Production db push 적용(§15 직렬화)
  • 설계서: 이슈 #1033 본문·스레드(Phase 설계 + 예상 효과)
  • 정본: docs/contracts/identity-passing.md — 세 가지 자리 규칙·적용 장부·게이트 명세
  • 도구: 정본 추출-치환 빌더(Production pg_get_functiondef 28건 + 고정 기반 schema 스냅샷), Production 원자 드라이런(DO-raise) + 행위 프로브
  • 게이트: pgTAP identity_passing_contract.test.sql(신원 전환 0·기본값 무신원·auth.uid() 최대 1회·내부 장부 43종) — migration-smoke 레인 상주
  • 버그리포트: 없음(결함 수리가 아닌 구조 트랙 — 발단인 친구 리포트 0kg은 #1028에서 종결)
  • 계약: 신원은 문(클라이언트 호출 함수)에서 auth.uid()를 딱 한 번 읽고, 내부는 명시 인자(p_actor/p_viewer/p_user_id)만 받는다. set_config로 다른 계정·역할인 척하는 코드는 금지.

한 줄 요약 — 서버 함수 곳곳에서 "요청자를 바꿔치기(JWT 클레임 스왑)"하거나 "함수 안에서 몰래 현재 로그인을 다시 읽는" 방식으로 처리되던 신원을, 전부 "문에서 한 번 확인하고 명시 인자로 전달"로 바꿨다. 신원 전환 11함수→0, 내부 신원 직독 38함수→0, role 클레임 셋업 3함수→0. 위반은 pgTAP 게이트가 CI에서 거부한다.

Phase 현황

Phase내용상태
Phase 0계약 문서(docs/contracts/identity-passing.md) 정본화✅ #1035
Phase 1통계 갱신 체인 17함수 + 트리거 전환 7곳 제거✅ #1035, Production 적용
Phase 2저장 체인(쓰기 엔진 4·검증기 2·문 4·온보딩 3)✅ #1037 (20260901830000)
Phase 3인입 스위트(WodUp 문 4·엔진 2·remap/백필 스왑 제거·검증 엔진 신설)✅ #1037 (20260901840000)
Phase 4읽기 코어(PR 상세·로그 테이블·달력·검색·발표 mains·세트 지표)✅ #1037 (20260901850000)
Phase 5잔재 청소(cron role 셋업·2회 읽기·인자 기본값) + 상시 게이트✅ #1037 (20260901850000)

1. 배경

친구 리포트 월별 총합 0kg(#1028)을 수리하면서, 친구 데이터를 "대상 유저로 로그인한 척 바꿔서 기존 함수를 부르는" 방식이 드러났다. 오너 결정으로 #1028은 전환 없는 전용 접근점으로 재설계했고, 같은 원리(계정인 척, 역할인 척)가 앱의 다른 곳에도 있는지 조사한 결과 통계 배치·관리자 인입·백필·cron 등 11개 함수가 같은 패턴을 쓰고 있었다. 오너가 이 원칙의 전면 적용을 지시했다(#1033).

2. 문제 제기

  • 계정 바꿔치기: 통계 재계산·인입 도구가 set_config('request.jwt.claim.sub', …)로 요청자를 대상 유저로 바꿔치기한 뒤 기존 함수를 호출했다. 복원을 잊으면 이후 코드가 남의 계정으로 돌고, 예외 경로에서 누가 누구인지 보증이 없다.
  • 역할인 척: cron 래퍼·가입 트리거가 role='service_role'을 꾸며 하위 게이트를 통과했다 — 게이트가 pg_cron 세션(role 값이 'none')을 인식하지 못한 것을 셋업으로 달랜 것.
  • 몰래 다시 읽기: 내부 계산 함수 38곳이 자기 안에서 auth.uid()를 다시 읽었다. definer 체인(그룹 보드 등)에서는 이 값이 호출 문맥에 따라 달라져, 예를 들어 세션 상세의 무게 지표가 누가 만들었는지에 따라 빠지는 실제 이상이 있었다.

3. 해결 방안

원칙(오너 D1, #1033): 신원은 문에서 딱 한 번. 클라이언트가 부르는 문(door)만 auth.uid()를 읽고 권한을 검사한 뒤, 내부(엔진·코어)는 "누구의 데이터를, 누가 보는가"를 전부 명시 인자로 받는다. 트리거·배치는 행 데이터·작업 큐의 user_id를 그대로 전달한다.

대안판정
전환 코드에 복원 가드만 보강기각 — 패턴 자체가 위험(예외 경로 보증 불가), 오너가 구조 제거를 선택
함수마다 개별 수리기각 — "비용 고려하지 말고 전면 적용" 지시, 부류별 일괄 재발행이 누락을 막는다
부류별 Phase(통계→저장→인입→읽기→청소) + 상시 게이트채택

4. 적용한 내용

  • Phase 1 통계 갱신 체인: 내부 12함수의 전환·주변 읽기 제거, 문 4곳 1회 읽기 정규화, 트리거의 전환 7곳 제거.
  • Phase 2 저장 체인: 쓰기 엔진 4종·검증기 2종에 p_actor 선행 인자(서명 변경은 drop 후 재생성 — #938 오버로드 함정), 문은 관통만. 온보딩 v2→v3→v4 체인 p_user_id 관통.
  • Phase 3 인입 스위트: WodUp 문 4종은 "특권 호출만 p_user_id 채택, 아니면 본인" 1회 해석으로 정규화. remap·백필·remove_import·motra의 전환 스캐폴딩 전면 제거. 내부 루프가 문을 다시 부르다 게이트에 걸리는 문제는 검증 엔진 신설(validate_user_exercise_stats_integrity_engine_v1)로 구조 해소 — 내부는 내부를 부른다.
  • Phase 4 읽기 코어: PR 상세 계열 내부 9종+문 4종, 로그 테이블, 계획 상세, 세션 검색, 발표 mains·세트 지표 맵(+p_viewer). 세션 상세 빌더는 지표 뷰어를 세션 소유자로 고정 — 그룹 보드 등에서 지표가 호출 문맥에 좌우되던 이상의 구조적 해소.
  • Phase 5 잔재 청소: 워커 문 2종의 특권 판정에 session_user 인식을 넣어 pg_cron을 직접 통과시키고, cron 래퍼 2종·가입 트리거의 role 셋업 제거. 2회 읽기 문 8곳 정리, validator 2종의 인자 기본값 auth.uid() 제거. 상시 게이트 pgTAP 신설.

작업 중 드러난 것:

  • DB 기본권한(default privileges)이 재생성 함수에 service_role 실행권을 자동 부여 — drop 후 재생성한 내부는 명시 revoke 없이는 위생 핀(internal_surface_grant_hygiene)을 위반한다. 실측으로 확정하고 내부 전부에 revoke를 명시했다.
  • 같은 함수 다중 트랙 재발행 되덮기가 실제로 한 번 발생 — Phase 5가 Production(묶음 B 적용 전) 본문을 기반으로 워커 문을 재발행해 묶음 B의 엔진 재조준을 되돌릴 뻔했고, 로컬 계약 테스트가 적발했다. 빌더 기반을 묶음 B 산출물로 교체해 해소.
  • pg_cron 게이트 미인식의 원인: 특권 판정 coalesce 체인이 role GUC 'none'에서 멈춰 session_user까지 내려가지 않았다 — 셋업(꾸미기)이 아니라 판정(인식)을 고치는 것이 맞는 방향.

5. 적용 결과

항목
신원 전환(set_config 계정 스왑) 함수110
내부 함수의 신원 직독(auth.uid)380
role 클레임 셋업 함수30
인자 기본값의 auth.uid()20
auth.uid() 2회 이상 읽는 함수80
위반 감시없음pgTAP 게이트 상주(6단언)

검증: Production 원자 드라이런 + 행위 프로브(저장 문 관통 22023/42501, 무클레임 엔진 호출, cron 셋업 제거 후 통과, 실제 유저 클레임으로 달력·검색·모더레이션) 전부 OK. 로컬 check 2,271/2,271. CI(verify+migration-smoke) 초록. 시각·감각 품질에는 영향이 없는 서버 구조 트랙이라 화면 확인 대상 없음.

6. 이번 개선으로 향상된 것

  • 계정 간 격리 보증: "누가(행위자)·누구의 데이터(대상)·누가 보는가(뷰어)"가 모든 내부 함수의 서명에 드러난다. 예외 경로에서 남의 계정으로 남는 종류의 사고가 구조적으로 불가능해졌다.
  • definer 체인의 결정성: 그룹 보드 완료 반영처럼 다른 사람 문맥에서 도는 저장·조회도 항상 소유자 기준으로 계산된다.
  • 구조적으로 남는 것: identity-passing.md 계약 + pgTAP 상시 게이트(새 함수의 위반을 CI가 거부) + "내부는 내부를 부른다" 원칙(검증 엔진 분리 패턴).

남은 것

  • 없음 — Phase 0~5 전부 랜딩·Production 적용·게이트 상주. 관리자 "유저로 보기"(앱 계층 임퍼서네이션)는 계약 §2대로 의도된 예외로 유지.