forked from baron/baron-sso
252 lines
17 KiB
Markdown
252 lines
17 KiB
Markdown
# Identity Redis Mirror 정책
|
|
|
|
관련 이슈: #1039
|
|
|
|
## 결정
|
|
|
|
사용자 identity에 대해 Backend DB 복제본이나 claim output을 SSOT 일치 보장 대상으로 취급하지 않습니다.
|
|
|
|
Baron SSO의 identity 원장은 Ory Kratos입니다. Redis는 Kratos identity를 빠르게 조회하기 위한 mirror/cache 계층이고, Backend DB는 Ory에 저장되지 않거나 Ory API로 필요한 방식의 조회가 불가능한 데이터의 read model로만 사용합니다.
|
|
|
|
Ory에서 Redis cache로 웜업된 identity/조직 데이터는 frontend나 외부 API가 직접 소비하지 않습니다. Backend가 Redis와 허용된 read model을 조합해 cursor 기반 API로 제공합니다.
|
|
|
|
## 역할 분리
|
|
|
|
| 구성요소 | 역할 | 보장 |
|
|
| --- | --- | --- |
|
|
| Kratos `identities` | identity SSOT | 인증 주체, credentials, recovery/verifiable address의 원장 |
|
|
| Redis identity mirror | cache/read mirror | 빠른 목록/검색/단건 조회. stale 가능 |
|
|
| Backend DB read model | Ory 보완 read model | Ory에 저장되지 않거나 조회 불가능한 업무/운영 데이터 |
|
|
|
|
Backend DB read model의 visible count를 Kratos identity total로 표시하지 않습니다. 화면과 API에서는 identity mirror count와 허용된 read model count를 분리해서 보여야 합니다.
|
|
|
|
## 현재 필드 대조
|
|
|
|
현재 코드 기준으로 Kratos traits와 backend DB `users`는 일부 필드를 중복 보관합니다. Redis mirror 전환 이후에는 Kratos traits를 인증/기본 identity 필드 중심으로 줄이고, Baron 업무/조직/연동 정보는 Ory에 저장되지 않거나 조회가 불가능한 경우에만 Backend read model로 유지합니다.
|
|
|
|
### Kratos에 유지할 identity 필드
|
|
|
|
| 필드 | 현재 저장 위치 | 유지 이유 |
|
|
| --- | --- | --- |
|
|
| `id` | Kratos identity ID, backend `users.id` 참조 | 인증 subject. WORKS `externalKey` 기준 |
|
|
| `email` | Kratos traits, backend `users.email` | 로그인 ID, recovery, verification |
|
|
| `phone_number` | Kratos traits, backend `users.phone` | SMS 로그인/검증 식별자 |
|
|
| `custom_login_ids` | Kratos traits, backend `user_login_ids` | password identifier. backend는 중복/정책 검증용 index |
|
|
| `name` | Kratos traits, backend `users.name` | 기본 프로필 표시 |
|
|
| `role` | Kratos traits, backend `users.role` | 세션/profile claim 계산에 필요. 최종 권한은 Keto/ReBAC와 함께 검증 |
|
|
| `state` | Kratos identity state, backend `users.status`로 일부 투영 | identity 활성 상태의 원본 |
|
|
|
|
### 현재 Kratos에도 있지만 backend-only로 축소해야 하는 필드
|
|
|
|
| 필드 | 현재 코드 사용 | 전환 방향 |
|
|
| --- | --- | --- |
|
|
| `tenant_id` | 대표 테넌트, profile, local user sync | Keto relation과 Backend read model 기준으로 이동. Kratos에는 identity 원장 필드로 추가하지 않음 |
|
|
| `department` | 사용자 표시, 조직도, WORKS 비교 | backend `users.department` 또는 tenant membership metadata 기준 |
|
|
| `grade` | 직급 표시, role fallback legacy | backend `users.grade`. role fallback 용도 제거 |
|
|
| `position` | 직책 표시 | backend `users.position` |
|
|
| `jobTitle` | 직무 표시 | backend `users.job_title` |
|
|
| `affiliationType` | 내부/외부/게스트 구분 | backend `users.affiliation_type` |
|
|
| `relying_party_id` | RP admin profile 보조 | backend/RP relation 기준 |
|
|
| `additionalAppointments` | 다중 소속 표시/WORKS 연동 | Keto relation과 Backend read model 기준 |
|
|
| `sub_email`, `aliasEmails`, `secondary_emails`, `worksmobileAliasEmails` | WORKS alias 및 보조 이메일 | backend `users.metadata` 또는 명시 테이블 기준 |
|
|
| tenant UUID namespaced metadata | tenant별 custom schema 값 | backend `users.metadata` 또는 전용 custom-field storage 기준 |
|
|
|
|
### Backend read model로만 허용하는 정보
|
|
|
|
| 데이터 | 저장 위치 | Ory SSOT와의 관계 |
|
|
| --- | --- | --- |
|
|
| soft-delete 상태 | `users.deleted_at` | Ory Kratos identity 삭제/비활성화와 의미가 다른 Baron 운영 상태 |
|
|
| Baron 사용자 상태 세부값 | `users.status` | WORKS provision/deprovision, org visible 정책과 결합된 운영 데이터 |
|
|
| WORKS mapping/outbox/job 상태 | `worksmobile_*` 테이블 | 외부 SaaS 연동 상태이며 identity 원장이 아님 |
|
|
| Keto outbox 및 relation sync 상태 | `keto_outboxes`, Keto | 권한/관계 원장은 Keto이고 DB는 처리 상태 read model |
|
|
| RP metadata/consent/usage | `rp_user_metadata`, `client_consents`, usage tables | Ory에 저장되지 않거나 client 단위 조회가 불가능한 RP 업무 데이터 |
|
|
| tenant tree 표시/검색 metadata | `tenants`, relation/outbox | 관계 판단은 Keto, 표시/검색/slug 조회는 Backend read model |
|
|
| custom field schema 및 tenant별 값 | tenant config, `users.metadata`, related tables | Ory에 schema/검색 정책을 저장하거나 조회할 수 없는 tenant별 운영 데이터 |
|
|
| `user_login_ids` row metadata | `user_login_ids` | Kratos는 identifier 값 원장, 발급 tenant/field key는 Backend 검증용 read model |
|
|
| audit/session activity read model | audit/clickhouse/local tables | 감사/운영 분석 데이터 |
|
|
|
|
정리하면, Kratos에는 “로그인과 subject 확인에 필요한 최소 identity”만 남깁니다. 조직도/WORKS/RP/Keto/감사/tenant custom schema에 필요한 데이터도 Ory에 저장되거나 조회 가능한 경우에는 Ory를 기준으로 하고, 그렇지 않은 영역만 Backend read model을 허용합니다.
|
|
|
|
## 일관성 모델
|
|
|
|
Redis mirror는 strong consistency 원장이 아닙니다.
|
|
|
|
다만 Baron backend를 통해 발생한 Kratos write에 대해서는 다음 수준을 목표로 합니다.
|
|
|
|
1. Kratos create/update/delete 성공
|
|
2. 성공한 identity ID를 기준으로 Kratos `GetIdentity(id)` 재조회
|
|
3. Redis `identity:mirror:{id}` write-through
|
|
4. Redis index/state 갱신
|
|
5. Redis 갱신 실패 시 mirror state를 `failed` 또는 `stale`로 표시
|
|
|
|
Kratos Admin API를 backend 밖에서 직접 수정하는 경로는 운영 정책상 금지합니다. 금지할 수 없는 환경에서는 Redis mirror가 stale해질 수 있음을 인정하고, 주기 refresh와 drift report로만 복구합니다.
|
|
|
|
## Kratos write 경로 감사
|
|
|
|
2026-06-09 기준으로 `admin/identities`, `CreateUser`, `UpdateIdentity`, `DeleteIdentity`, `UpdateUserPassword`, Kratos DB `identities` 직접 변경 경로를 정적 검색했습니다.
|
|
|
|
### 중앙 Kratos client 구현
|
|
|
|
| 파일 | 역할 | 판정 |
|
|
| --- | --- | --- |
|
|
| `backend/internal/service/identity_write_service.go` | Kratos identity 변경의 중앙 write boundary. 성공/실패 후 Redis mirror 상태를 갱신 또는 stale 표시 | 허용. 신규 identity write는 이 서비스를 거쳐야 함 |
|
|
| `backend/internal/service/kratos_admin_service.go` | Kratos Admin API list/get/create/update/delete/password/session client | 허용. 이후 `IdentityWriteService`의 하위 client로만 사용 |
|
|
| `backend/internal/service/ory_service.go` | legacy IDP provider. create/password/verifiable address 변경 시 Kratos Admin API 호출 | 허용하되 write-through 책임은 상위 `IdentityWriteService`로 이동 |
|
|
|
|
### 정상 backend API 경로
|
|
|
|
아래 경로는 사용자 요청이 backend를 통과하지만, Redis mirror write-through 구현 시 모두 같은 중앙 write service를 지나야 합니다.
|
|
|
|
| 경로 | 파일 | Kratos 변경 | 판정 |
|
|
| --- | --- | --- | --- |
|
|
| admin 사용자 단건 생성 | `backend/internal/handler/user_handler.go` | `OryProvider.CreateUser` | 허용. 생성 성공 후 Kratos 재조회와 Redis mirror write-through 필요 |
|
|
| admin 사용자 bulk 생성 | `backend/internal/handler/user_handler.go` | `OryProvider.CreateUser` | 허용. 부분 성공/실패별 mirror 갱신 필요 |
|
|
| admin 사용자 수정 | `backend/internal/handler/user_handler.go` | `KratosAdmin.UpdateIdentity`, 선택적 `OryProvider.UpdateUserPassword` | 허용. password 변경도 identity write audit에 포함 |
|
|
| admin 사용자 삭제/bulk 삭제 | `backend/internal/handler/user_handler.go` | `KratosAdmin.DeleteIdentity` | 허용. Redis mirror delete 또는 tombstone 갱신 필요 |
|
|
| 일반 회원가입 | `backend/internal/handler/auth_handler.go` | `IdpProvider.CreateUser` | 허용이지만 Backend read model sync가 goroutine 기반이라 write-through 기준에서는 약함 |
|
|
| 내 프로필 수정 | `backend/internal/handler/auth_handler.go` | `KratosAdmin.UpdateIdentity` | 직접 `PUT /admin/identities/{id}` 호출 제거 완료. 향후 `IdentityWriteService` write-through 대상 |
|
|
| 비밀번호 재설정/내 비밀번호 변경 | `backend/internal/handler/auth_handler.go` | `IdpProvider.UpdateUserPassword` | 허용. traits mirror와 별도 audit event 필요 |
|
|
| 조직 그룹 멤버 추가 | `backend/internal/service/user_group_service.go` | Kratos write 없음 | Kratos `tenant_id`, `department` write 제거 완료. 조직/부서 정보는 backend DB/Keto/WORKS 기준 |
|
|
|
|
### backend 내부이지만 일반 API가 아닌 경로
|
|
|
|
| 경로 | 파일 | Kratos 변경 | 판정 |
|
|
| --- | --- | --- | --- |
|
|
| super-admin 보장 CLI | `backend/cmd/adminctl/main.go`, `backend/internal/bootstrap/admin_account.go` | `CreateUser`, `UpdateIdentityPassword` | 운영 bootstrap/정비 경로. 실행 후 Redis mirror 갱신 또는 refresh 필수 |
|
|
| 초기 admin seed | `backend/internal/bootstrap/kratos_seed.go` | `IdpProvider.CreateUser` | startup bootstrap 경로. 신규 환경에서만 허용하고 반복 실행 영향 점검 필요 |
|
|
| role 보정 CLI | `backend/cmd/fix_kratos_roles.go` | `ListIdentities` 후 `UpdateIdentity` | 기본 dry-run. 실제 변경은 `--dry-run=false --maintenance-window --mark-mirror-stale` 없이는 거부 |
|
|
| WORKS 기준 Baron 보정 CLI | `backend/cmd/adminctl/worksmobile_sync.go` | `IdentityWriteService.UpdateIdentity` | 중앙 write boundary 강제. 변경 후 Redis mirror stale 표시 |
|
|
| RP custom claim traits sync | `backend/internal/handler/dev_handler.go` | `IdentityWriteService.UpdateIdentity` | 중앙 write boundary 강제. RP read model과 Kratos traits 동기화 잔여 경로는 Ory SSOT 전환 대상 |
|
|
|
|
### backend와 Kratos Admin API를 모두 우회하는 경로
|
|
|
|
| 경로 | 파일/문서 | 변경 대상 | 판정 |
|
|
| --- | --- | --- | --- |
|
|
| orphan tenant membership shell 정리 | `scripts/clear_orphan_tenant_memberships.sh` | `ory_kratos.identities.traits` 직접 `UPDATE` | `CONFIRM_KRATOS_DB_MAINTENANCE=baron-sso`와 `MARK_IDENTITY_MIRROR_STALE=true` 없이는 거부 |
|
|
| tenant maintenance 문서의 직접 SQL 절차 | `docs/tenant-maintenance-procedures.md` | Baron DB 및 Kratos traits | 문서화된 우회 절차. Kratos DB 직접 UPDATE 절차는 폐기 또는 강한 경고 필요 |
|
|
| backup restore | `scripts/backup/restore.sh` | `ory_kratos` 전체 restore 가능 | DR 경로로만 허용. restore 이후 Redis mirror full rebuild와 Baron DB/Kratos drift report 필수 |
|
|
| Docker network 직접 접근 | `compose`, `docker`, `mcp` 설정 | `http://kratos:4434` 접근 가능 컨테이너 | public publish는 아니지만 같은 network의 정비 컨테이너가 admin API를 칠 수 있음. 접근 주체 제한 필요 |
|
|
|
|
### 조치 원칙
|
|
|
|
- Kratos identity write는 `IdentityWriteService` 하나로 모으고, 성공한 create/update/delete/password 변경이 audit와 Redis mirror write-through를 남기게 합니다.
|
|
- `auth_handler.updateKratosIdentity`처럼 `KRATOS_ADMIN_URL`을 직접 읽어 `admin/identities`를 호출하는 코드는 금지합니다.
|
|
- `backend/cmd/fix_kratos_roles.go`와 Kratos DB 직접 UPDATE 스크립트는 `--dry-run`, `--maintenance-window`, `--mark-mirror-stale` 같은 명시적 가드 없이는 실행하지 못하게 합니다.
|
|
- shell/SQL로 Kratos DB를 직접 수정한 경우에는 Backend read model이나 Redis mirror를 신뢰하지 않고, Kratos full refresh와 drift report를 먼저 실행합니다.
|
|
- CI에 정적 정책 테스트를 추가해 `admin/identities` write 호출과 `UPDATE identities` SQL이 허용 파일 밖에 생기면 실패시킵니다.
|
|
|
|
## Redis 키 설계
|
|
|
|
초기 구현 기준:
|
|
|
|
- `identity:mirror:{identityID}`
|
|
- Kratos identity summary JSON
|
|
- 단건 조회 cache
|
|
- `identity:mirror:state`
|
|
- `status`: `ready`, `refreshing`, `stale`, `failed`
|
|
- `lastRefreshedAt`
|
|
- `lastError`
|
|
- `observedCount`
|
|
- `identity:index:active`
|
|
- active identity ID 목록
|
|
- 구현은 Redis Set 또는 Sorted Set으로 둡니다.
|
|
|
|
기존 `domain.RedisRepository`가 문자열 KV만 제공하므로 목록 index를 제대로 구현하려면 Redis repository 인터페이스를 확장해야 합니다.
|
|
|
|
## API 원칙
|
|
|
|
사용자 목록 API는 다음 값을 구분해야 합니다.
|
|
|
|
- `identityTotal`: Redis mirror 기준 Kratos identity 수
|
|
- `localUserTotal`: PostgreSQL `users` 기준 Baron 로컬 사용자 수
|
|
- `mirrorStatus`: Redis mirror 상태
|
|
- `items`: identity mirror와 허용된 Backend read model을 조합한 응답
|
|
|
|
Redis cache miss 발생 시:
|
|
|
|
1. 단건 조회는 Kratos `GetIdentity(id)`로 fallback합니다.
|
|
2. fallback 성공 시 Redis mirror를 갱신합니다.
|
|
3. fallback 실패 시 SSOT 조회 실패로 응답합니다.
|
|
|
|
목록 조회는 Redis mirror가 `ready`가 아니면 경고 상태를 포함해야 합니다. Backend read model을 대체 SSOT처럼 사용하지 않습니다.
|
|
|
|
## Front 전송과 cursor 보장
|
|
|
|
front/API로 전달되는 사용자 목록은 Backend가 제공하는 cursor 기반을 원칙으로 합니다. offset은 하위 호환 파라미터로만 유지하고, 신규 화면 또는 대량 조회 화면은 cursor 외 방식을 사용하지 않습니다.
|
|
|
|
### API 계약
|
|
|
|
`GET /api/v1/admin/users` 응답은 다음 형태를 유지해야 합니다.
|
|
|
|
```json
|
|
{
|
|
"items": [],
|
|
"limit": 50,
|
|
"cursor": "현재 요청 cursor 또는 빈 값",
|
|
"nextCursor": "다음 페이지 cursor 또는 빈 값",
|
|
"identityTotal": 2106,
|
|
"localUserTotal": 2106,
|
|
"mirrorStatus": "ready"
|
|
}
|
|
```
|
|
|
|
전환 기간에는 기존 `total`을 유지할 수 있지만 의미를 명확히 해야 합니다.
|
|
|
|
- `identityTotal`: Redis identity mirror 기준 Kratos identity 수
|
|
- `localUserTotal`: PostgreSQL `users` 기준 Baron business local record 수
|
|
- `total`: deprecated. 화면에서 신규 의미로 사용하지 않습니다.
|
|
|
|
### cursor 불변 조건
|
|
|
|
- `nextCursor`가 있으면 front는 반드시 다음 요청에 `cursor=nextCursor`를 사용합니다.
|
|
- cursor 요청에서는 `offset`을 의미 있게 사용하지 않습니다.
|
|
- backend는 cursor 요청에서 안정 정렬 키를 사용해야 합니다. 기본 정렬은 `created_at DESC, id DESC` 또는 mirror index score + `id` 같이 중복 없는 조합이어야 합니다.
|
|
- cursor는 필터 조건(`search`, `tenantSlug`, status filter 등)과 묶여야 합니다. 다른 필터에 재사용하면 400으로 거부할 수 있습니다.
|
|
- 대량 화면도 `limit=5000&offset=0` 단일 호출을 사용하지 않습니다.
|
|
|
|
### 현재 front 점검 결과
|
|
|
|
| 화면/모듈 | 현재 상태 | 조치 |
|
|
| --- | --- | --- |
|
|
| `adminfront` 사용자 목록 | `useInfiniteQuery`로 `nextCursor` 사용 | 유지 |
|
|
| `adminfront` 일부 tenant/user group modal | `fetchUsers(20/100/1000, 0)` 단일 호출 | cursor helper로 전환 |
|
|
| `adminfront` bulk upload modal | `fetchUsers(10000, 0)` 단일 호출 | 금지. cursor 수집 helper 또는 서버 검증 API로 전환 |
|
|
| `orgfront` 조직도 | Redis orgchart snapshot 기반 | 유지. Backend가 Ory/Redis/read model을 조합해 제공 |
|
|
| `orgfront` 조직 picker | Redis orgchart snapshot 기반으로 전환 | 유지. 인증 picker는 `fetchOrgChartSnapshot`, public picker는 token 기반 orgchart API 사용 |
|
|
| `orgfront/src/lib/adminApi.ts` | `UserListResponse`에 `nextCursor` 없음 | 타입 계약 보완 |
|
|
|
|
공통 helper 원칙:
|
|
|
|
- `fetchUsersPage(params)`는 단일 페이지 조회만 담당합니다.
|
|
- `fetchAllUsersByCursor(params)`는 `nextCursor`가 없어질 때까지 반복합니다.
|
|
- front에서 arbitrary large limit로 전체 데이터를 가져오는 코드를 금지합니다.
|
|
- E2E 또는 unit test에서 `fetchAllUsersByCursor`가 `nextCursor`를 끝까지 따라가는지 검증합니다.
|
|
|
|
## Refresh 정책
|
|
|
|
주기 refresh는 Redis mirror를 재구성하는 best-effort 작업입니다.
|
|
|
|
Kratos list pagination을 끝까지 순회하는 것은 refresh의 필요조건일 뿐입니다. 호출 도중 Kratos identity 변경이 끼어들 수 있으므로 snapshot isolation 보장으로 해석하지 않습니다.
|
|
|
|
refresh 중 불일치 또는 실패가 발생하면:
|
|
|
|
- Redis mirror state를 `failed` 또는 `stale`로 표시합니다.
|
|
- PostgreSQL `users`를 자동 삭제하지 않습니다.
|
|
- drift report를 남겨 운영자가 확인할 수 있게 합니다.
|
|
|
|
## 금지 사항
|
|
|
|
- Kratos partial list를 full snapshot으로 간주하지 않습니다.
|
|
- Backend read model을 Kratos identity total의 원장으로 사용하지 않습니다.
|
|
- Redis mirror refresh 실패를 숨기고 `ready`로 표시하지 않습니다.
|
|
- 외부 도구가 Kratos Admin API를 직접 수정하도록 허용하지 않습니다.
|
|
|
|
## 전환 작업
|
|
|
|
1. legacy user sync 명칭과 API를 `identity_mirror` 성격으로 분리합니다.
|
|
2. Redis repository에 Set/Sorted Set 또는 scan 가능한 index 연산을 추가합니다.
|
|
3. Kratos create/update/delete 성공 직후 Redis write-through 테스트를 추가합니다.
|
|
4. admin 사용자 목록 응답에서 identity count와 local user count를 분리합니다.
|
|
5. 기존 Backend DB count를 identity count처럼 사용하는 화면과 WORKS 비교 경로를 점검합니다.
|
|
6. Kratos identity 변경은 `IdentityWriteService` 경유를 강제하고, 직접 `KratosAdmin.UpdateIdentity` 경로를 정책 테스트로 차단합니다.
|