Files
BaronSSO/baron-sso/docs/user-projection-visibility-audit-2026-06-08.md

6.3 KiB

사용자 projection 가시성 감사 보고서

작성 시각: 2026-06-08 16:55 KST

관련 이슈:

  • #1035: adminfront 사용자 레지스트리 total이 Kratos 250건 제한으로 잘못 표시됨
  • #1036: 사용자 projection 가시성 영향 범위 검증 및 WORKS 비교 표 row count 표시

결론

총 250명 표시는 단순 UI 문제가 아니라, Kratos partial list를 full snapshot처럼 처리한 projection 동기화 버그였습니다.

현재 로컬 DB와 API는 다음 상태입니다.

항목 건수 설명
users 전체 2,114 deleted_at 포함 전체 row
visible users 1,917 deleted_at IS NULL, adminfront/orgfront 사용자 목록 기준
soft-deleted users 197 사용자 삭제 또는 과거 projection 문제로 숨겨진 row
CSV 원본 줄 수 1,887 헤더 포함
CSV 실제 데이터 행 1,886 헤더 제외
이번 import 사용자 1,886 모두 DB 매칭, 모두 visible
import ID에 없는 기존 사용자 228 visible 31, soft-deleted 197

따라서 “기존 221명 + 신규 1887명” 계산은 그대로 적용하면 안 됩니다.

  • 1887은 CSV 헤더 포함 줄 수이고 실제 신규 데이터 행은 1886입니다.
  • 현재 DB에서 import ID에 없는 기존 사용자는 228명입니다.
  • 그중 197명은 soft-delete 상태라 adminfront/orgfront visible total에는 포함되지 않습니다.
  • 현재 화면 기준 총 사용자는 신규 visible 1886 + 기존 visible 31 = 1917입니다.

생성한 대조 파일

보고 파일 위치:

  • reports/user-projection-visibility-audit-20260608-1645/existing_users_not_in_saman_import.csv
  • reports/user-projection-visibility-audit-20260608-1645/imported_users_missing_or_soft_deleted.csv

파일 내용:

  • existing_users_not_in_saman_import.csv: 이번 CSV import ID에 없는 기존 사용자 228명 전체 목록입니다.
  • imported_users_missing_or_soft_deleted.csv: 이번 import 사용자 중 DB 누락 또는 soft-delete 상태인 사용자 목록입니다. 현재는 헤더만 있고 데이터 row는 0건입니다.

삭제 정책 확인

adminfront 사용자 삭제는 다음 순서로 동작합니다.

  1. Kratos identity 삭제를 시도합니다.
  2. WORKS 연동 범위 사용자이면 WORKS delete outbox를 enqueue합니다.
  3. 로컬 users row는 GORM Delete로 soft-delete 합니다.

따라서 adminfront에서 사용자 삭제를 했다고 로컬 DB row가 hard-delete 되는 구조는 아닙니다. users.deleted_at에 값이 들어가고, 일반 조회에서는 제외됩니다.

예외적으로 사용자 생성/재생성 시 email unique 충돌을 풀기 위한 Unscoped hard-delete 경로가 일부 존재합니다. 이 경로는 일반 사용자 삭제 정책과 다릅니다.

영향 범위 검증

adminfront 사용자 레지스트리

  • GET /api/v1/admin/users?limit=50&offset=0
  • 응답 total: 1917
  • adminfront /users 화면 문구: 총 1917명의 사용자가 등록되어 있습니다.

orgfront 사용자 소비 경로

현재 orgfront 조직도와 picker는 fetchUsers(5000, 0) 형태로 사용자 목록을 한 번 가져옵니다.

검증 결과:

  • GET /api/v1/admin/users?limit=5000&offset=0
  • items: 1917
  • total: 1917
  • soft-deleted 기존 사용자 197명 중 응답 포함: 0

현재 visible 사용자가 5,000명 미만이라 orgfront의 현 방식은 전체 visible 사용자를 모두 받습니다. 다만 사용자가 5,000명을 넘으면 partial 조회가 될 수 있으므로 cursor 기반 전환이 필요합니다.

WORKS comparison API와 화면

WORKS comparison은 Baron visible 사용자뿐 아니라 WORKS에만 존재하는 remote row도 함께 보여주는 비교 화면입니다. 따라서 사용자 목록 visible total과 1:1로 일치하지 않습니다.

includeMatched=true 기준 최신 API 결과:

구분 전체 matched missing_in_worksmobile missing_external_key missing_in_baron
users 2,110 1,874 41 6 189
groups 187 185 1 - 1

WORKS 화면 row count 표시:

  • 구성원: 표시 232 / 전체 2110
  • 조직/그룹: 표시 2 / 전체 187

구성원 표시 232는 기본 필터와 보호 계정 제거가 적용된 화면 노출 row 수입니다. 전체 2110은 API comparison row 전체입니다.

soft-deleted 기존 사용자 197명과 WORKS comparison baronId를 대조한 결과:

  • soft-deleted Baron row 포함: 0

왜 업데이트가 많았는가

이번 문제는 단일 UI 카운트 문제가 아니라 다음 경계가 한꺼번에 얽힌 문제였습니다.

  1. Kratos identity list는 partial list인데 backend projection sync가 full snapshot으로 처리했습니다.
  2. projection sync가 local DB soft-delete까지 수행해 사용자 가시성 자체를 손상시켰습니다.
  3. adminfront 사용자 목록, orgfront 조직도, WORKS comparison이 모두 사용자 projection을 다른 방식으로 소비하고 있었습니다.
  4. WORKS comparison은 사용자 목록이 아니라 Baron/WORKS 양쪽 차이를 보여주는 비교 화면이라 total 의미가 달랐습니다.
  5. 운영자가 partial data인지 바로 볼 수 있도록 WORKS 표 row count가 필요했습니다.

pagination 정리

현재 구조는 다음과 같습니다.

  • Ory/Kratos -> backend projection sync: 현재 ListIdentities() partial 조회입니다. offset/cursor 전체 순회가 아닙니다.
  • backend projection -> adminfront 사용자 목록: cursor가 있으면 cursor pagination, 없으면 offset pagination을 받습니다. adminfront는 infinite query로 nextCursor를 사용합니다.
  • backend projection -> orgfront 조직도/picker: 현재 limit=5000&offset=0 단일 offset 조회입니다.
  • WORKS comparison: backend가 비교 결과 배열을 만들어 내려주고, adminfront가 검색/필터 후 화면 row를 표시합니다.

재발 방지 조치

  • 사용자 목록 API는 Kratos가 아니라 local projection DB를 primary source로 사용합니다.
  • Kratos partial list에 없는 사용자를 projection sync에서 삭제하지 않도록 수정했습니다.
  • WORKS comparison에서 soft-deleted local user가 들어와도 comparison row로 노출되지 않도록 방어 테스트와 로직을 추가했습니다.
  • WORKS comparison 표에 표시 N / 전체 M row count를 표시했습니다.