forked from baron/baron-sso
124 lines
6.4 KiB
Markdown
124 lines
6.4 KiB
Markdown
# 사용자 가시성 감사 보고서
|
|
|
|
작성 시각: 2026-06-08 16:55 KST
|
|
|
|
관련 이슈:
|
|
- #1035: adminfront 사용자 레지스트리 total이 Kratos 250건 제한으로 잘못 표시됨
|
|
- #1036: 사용자 가시성 영향 범위 검증 및 WORKS 비교 표 row count 표시
|
|
|
|
## 결론
|
|
|
|
`총 250명` 표시는 단순 UI 문제가 아니라, Kratos partial list를 full snapshot처럼 처리한 legacy sync 버그였습니다.
|
|
|
|
현재 로컬 DB와 API는 다음 상태입니다.
|
|
|
|
| 항목 | 건수 | 설명 |
|
|
| --- | ---: | --- |
|
|
| users 전체 | 2,114 | `deleted_at` 포함 전체 row |
|
|
| visible users | 1,917 | `deleted_at IS NULL`, adminfront/orgfront 사용자 목록 기준 |
|
|
| soft-deleted users | 197 | 사용자 삭제 또는 과거 legacy sync 문제로 숨겨진 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-visibility-audit-20260608-1645/existing_users_not_in_saman_import.csv`
|
|
- `reports/user-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인데 legacy backend sync가 full snapshot으로 처리했습니다.
|
|
2. legacy sync가 Backend DB soft-delete까지 수행해 사용자 가시성 자체를 손상시켰습니다.
|
|
3. adminfront 사용자 목록, orgfront 조직도, WORKS comparison이 모두 사용자 데이터를 다른 방식으로 소비하고 있었습니다.
|
|
4. WORKS comparison은 사용자 목록이 아니라 Baron/WORKS 양쪽 차이를 보여주는 비교 화면이라 total 의미가 달랐습니다.
|
|
5. 운영자가 partial data인지 바로 볼 수 있도록 WORKS 표 row count가 필요했습니다.
|
|
|
|
## pagination 정리
|
|
|
|
현재 구조는 다음과 같습니다.
|
|
|
|
- Ory/Kratos -> Backend identity mirror warmup: `ListIdentities()` partial 조회를 전체 snapshot으로 취급하면 안 됩니다. 전체 수집은 pagination을 끝까지 따라가야 합니다.
|
|
- Backend -> adminfront 사용자 목록: `cursor`가 있으면 cursor pagination, 없으면 offset pagination을 받습니다. adminfront는 infinite query로 `nextCursor`를 사용합니다.
|
|
- Backend -> orgfront 조직도/picker: Redis orgchart snapshot 또는 Backend cursor API를 사용해야 하며 `limit=5000&offset=0` 단일 offset 조회는 금지합니다.
|
|
- WORKS comparison: backend가 비교 결과 배열을 만들어 내려주고, adminfront가 검색/필터 후 화면 row를 표시합니다.
|
|
|
|
## 재발 방지 조치
|
|
|
|
- 사용자 목록 API는 Backend가 Ory-warmed Redis cache와 허용된 read model을 조합해 cursor로 제공합니다.
|
|
- Kratos partial list에 없는 사용자를 legacy sync에서 삭제하지 않도록 수정했습니다.
|
|
- WORKS comparison에서 soft-deleted local user가 들어와도 comparison row로 노출되지 않도록 방어 테스트와 로직을 추가했습니다.
|
|
- WORKS comparison 표에 `표시 N / 전체 M` row count를 표시했습니다.
|