1
0
forked from baron/baron-sso

chore: snapshot local state before dev merge

This commit is contained in:
2026-06-17 21:25:42 +09:00
parent b2808759d2
commit 49560e8a8c
107 changed files with 8958 additions and 939 deletions

View File

@@ -8,7 +8,7 @@ Baron SSO에서 인증 identity, 권한 관계, OAuth/OIDC 위임의 원장은 O
- Authorization/ReBAC 원장: Ory Keto
- OAuth/OIDC client, consent, token state 원장: Ory Hydra
Backend DB는 Ory를 대체하는 원장이 아닙니다. Ory에 저장되지 않거나 Ory API로 필요한 방식의 조회가 불가능한 업무 데이터의 read model, 감사 로그, 처리 상태, 성능 cache 보조 데이터만 허용합니다.
Backend DB는 Ory를 대체하는 원장이 아닙니다. 특히 사용자 identity/profile/소속/조직도 노출 데이터에 대해 Backend DB `users`를 원장 또는 read model로 사용하지 않습니다. Ory와 무관한 감사 로그, 처리 상태, 외부 연동 작업 상태처럼 별도 원장이 명시된 데이터만 Backend DB에 둘 수 있습니다.
Ory에서 Redis cache로 웜업된 데이터는 Backend가 cursor 기반 API로 front 또는 외부 API에 제공합니다. frontend는 Redis나 Backend DB 복제본을 원장처럼 직접 소비하지 않습니다.
@@ -19,7 +19,7 @@ Ory에서 Redis cache로 웜업된 데이터는 Backend가 cursor 기반 API로
- Ory Kratos identity가 subject, credentials, recovery/verification address, 인증 식별자의 원장입니다.
- Kratos identity 변경은 Backend의 중앙 `IdentityWriteService`를 경유해야 합니다.
- Redis identity mirror는 빠른 단건/목록/검색 조회를 위한 cache입니다. stale 가능성을 API 응답에 드러내야 합니다.
- Backend DB `users`Ory에 저장되지 않거나 Ory에서 필요한 방식으로 조회할 수 없는 Baron 운영 데이터의 read model입니다.
- Backend DB `users`사용자 identity/profile/소속 조회 read model이 아닙니다. 남은 의존은 제거 대상이며, 조회 API는 Kratos identity mirror 또는 Kratos Admin API fallback을 기준으로 해야 합니다.
### 2.2 Permissions & Relationships
@@ -41,14 +41,14 @@ Ory에서 Redis cache로 웜업된 데이터는 Backend가 cursor 기반 API로
2. Backend가 중앙 service를 통해 Ory API를 동기 호출합니다.
3. Ory write 성공 후 Ory ID로 재조회합니다.
4. Redis mirror를 갱신하거나 갱신 실패 시 `stale`/`failed` 상태를 기록합니다.
5. Ory에 저장되지 않거나 조회 불가능한 read model만 Backend DB에 갱신합니다.
5. 사용자 identity/profile/소속 데이터는 Backend DB `users`에 read model로 갱신하지 않습니다. Ory와 별도 원장이 명시된 처리 상태만 Backend DB에 기록합니다.
### 3.2 Read Path
- Self context: Ory session/token 또는 Ory API를 기준으로 검증합니다.
- Admin/list context: Backend가 Redis mirror와 허용된 read model을 조합해 cursor 기반 API 제공합니다.
- Admin/list context: Backend가 Redis identity mirror 또는 Ory Admin API fallback을 기준으로 cursor 기반 API 제공합니다.
- API response는 `identityTotal`, read model count, mirror status를 구분해야 합니다.
### 3.3 Conflict Resolution
불일치가 발견되면 Ory Stack의 데이터를 기준으로 Redis mirror와 Backend read model을 보정합니다. Backend read model이나 token claim assembly 결과를 Ory보다 우선하는 근거로 사용하지 않습니다.
불일치가 발견되면 Ory Stack의 데이터를 기준으로 Redis mirror 보정합니다. Backend DB `users`나 token claim assembly 결과를 Ory보다 우선하는 근거로 사용하지 않습니다.

View File

@@ -0,0 +1,110 @@
# Admin User List SSOT, Cache, and Local User DB 제거 정책
작성일: 2026-06-17
## 결론
AdminFront 사용자 목록은 테넌트 목록과 같은 cursor/search 경험을 제공해야 하지만, 사용자 identity의 SSOT는 Ory Kratos입니다. 성능 목표를 위해 cache를 사용할 수 있고 사용해야 하지만, cache나 PostgreSQL `users` table을 사용자 identity/profile/소속 조회의 원장 또는 read model로 사용하면 안 됩니다.
테넌트 목록은 현재 primary DB cursor/search/sort query와 batch aggregation으로 개선되어 있으며, Redis cache는 필수 구현으로 들어가지 않았습니다. 다만 #1191의 정책처럼 Redis cache는 tenant tree edge, scope 계산, page response, member count aggregate 같은 보조 성능 계층으로 허용됩니다.
사용자 목록은 데이터 원장이 Kratos이고 목록 조회가 Redis identity mirror를 거치므로, 목표 수치에 도달하려면 단순 KV mirror 전체 scan이 아니라 cursor/search/scope에 맞는 Redis index cache가 필요합니다.
## 사용자 목록 SSOT 원칙
- Kratos `identities`가 subject, credentials, recovery/verifiable address, identity state의 원장입니다.
- Redis identity mirror/index는 Kratos identity를 빠르게 조회하기 위한 cache입니다.
- Backend DB `users`는 Kratos를 대체하는 사용자 원장도, 사용자 조회 read model도 아닙니다.
- 사용자 목록 API는 Kratos에서 warm-up된 Redis mirror를 기준으로 응답해야 하며, identity 존재 여부와 identity total은 Kratos mirror 기준을 우선해야 합니다.
- Redis mirror가 stale/failed/empty이면 이를 숨기지 말고 API 응답 또는 운영 상태에 드러내야 합니다.
- cache miss 또는 drift가 발견되면 Kratos 기준으로 Redis mirror를 보정해야 하며, local `users` DB를 정상 fallback처럼 사용하면 안 됩니다.
## Cache 사용 가능 범위
허용되는 cache:
- `identity:mirror:{identityID}`: Kratos identity summary JSON
- `identity:mirror:state`: mirror status, observed count, refreshed time, error
- sorted/index set: `createdAt,id` 기반 cursor page 조회
- normalized search token index: email, name, login ID, phone, selected metadata search
- tenant access key index: primary tenant, joined tenant, additional appointment tenant id/slug
- short TTL page response cache: role/scope/search/tenantSlug/sort/direction/cursor/limit 포함 key
제약:
- cache key는 query scope를 모두 포함해야 합니다.
- 권한 범위가 들어간 cache는 user id 또는 permission scope hash를 포함해야 합니다.
- Kratos write-through 실패 시 mirror state를 `stale` 또는 `failed`로 전환해야 합니다.
- Redis 장애 시 local DB를 identity SSOT fallback처럼 사용하지 않습니다. 가능한 경우 Kratos API fallback을 쓰고, 불가능하면 identity mirror unavailable로 실패시킵니다.
- cache는 감사 근거 또는 권한 판정 단독 근거가 아닙니다. 권한 관계의 원장은 Keto입니다.
## 현재 사용자 목록 병목
현재 `GET /v1/admin/users``listIdentitiesFromMirrorOrKratos()`를 통해 identity mirror 전체를 배열로 읽은 다음 handler 메모리에서 검색, tenant filter, 권한 scope, sort, pagination을 수행합니다.
Redis mirror 구현도 `SCAN identity:mirror:*` 후 key별 `GET`으로 전체 identity를 materialize합니다. 따라서 API 응답은 cursor를 반환하지만, 서버 내부 비용은 page 단위가 아니라 전체 mirror 크기에 비례합니다.
테넌트 목록과 같은 성능 특성을 만들려면 `/v1/admin/users`의 cursor/search/scope 조건이 Redis identity index 또는 Kratos-backed query boundary까지 내려가야 합니다.
## Local `users` DB 잔존 의존 제거 대상
PostgreSQL `users` table은 admin user list, org-context, orgfront snapshot의 사용자 identity/profile/소속 read model로 사용하면 안 됩니다. 현재 코드 기준으로 남아 있는 의존은 유지 근거가 아니라 제거 또는 별도 원장 재정의가 필요한 대상입니다.
| 기능 | 사용 위치 | 정리 방향 |
| --- | --- | --- |
| 사용자 생성/수정 후 local sync | `user_handler.go`, `auth_handler.go` | Kratos write-through 후 Redis mirror 갱신으로 대체하고 local sync 제거 여부를 추적합니다. |
| custom login ID index | `user_login_ids`, `IsLoginIDTaken`, `FindTenantIDByLoginID` | Kratos traits/credentials identifier 또는 별도 명시 원장으로 재정의합니다. |
| tenant membership count | `TenantHandler.countTenantMembers`, `UserRepo.CountByTenantIDs` | Redis identity mirror/Keto relation 기준 aggregate로 대체합니다. |
| org context member export | `TenantHandler.loadOrgContextMembers` | Kratos identity mirror 기준으로 전환합니다. |
| admin CSV export/import | `ExportUsersCSV`, bulk create/update | Kratos/mirror 기반 export와 command import로 분리합니다. |
| WORKS Mobile sync/comparison | `worksmobile_sync_service.go` | Kratos identity mirror와 WORKS API 비교로 전환합니다. |
| Hanmac email/local-part policy | `hanmac_email_policy.go` | Kratos identifier/mirror index 기준 uniqueness로 전환합니다. |
| user status 운영 정책 | `users.status`, `deleted_at` | Kratos traits/state 또는 명시된 별도 원장으로 재정의합니다. |
| profile/session 보조 | `auth_handler.go` | Kratos session/identity traits와 Redis mirror 기준으로 전환합니다. |
따라서 local `users` DB를 사용자 조회의 primary source 또는 read model로 유지하는 방향은 최신 정책과 맞지 않습니다.
## 사용자 목록 개선 방향
1. Redis identity mirror index를 확장합니다.
- 기본 정렬: `createdAt desc, id desc`
- cursor: timestamp + identity id
- search: normalized email/name/login ID/phone/token index
- tenant filter: primary tenant id/slug와 additional appointments 기반 index
2. `/v1/admin/users` query boundary를 분리합니다.
- 입력: `limit`, `cursor`, `search`, `tenantSlug`, `sort`, `direction`, requester scope
- 출력: page identities, `identityTotal`, `nextCursor`, `mirrorStatus`
- handler는 전체 identity slice를 직접 만들지 않습니다.
3. row enrichment는 page 단위 batch로 수행합니다.
- primary tenant summary lookup batch
- joined tenant/relation lookup batch 또는 bounded cache
- per-row `GetTenant`/`ListJoinedTenants` 반복 호출 금지
4. local `users` DB join을 제거합니다.
- 사용자 identity/profile/소속/상태는 Kratos mirror 기준으로 응답
- Kratos mirror에 없는 local DB row를 목록 identity로 승격하지 않음
- 남은 local DB 의존은 drift/deprecation report에만 노출
5. AdminFront는 tenant 목록과 같은 query lifecycle로 맞춥니다.
- `useInfiniteQuery` queryKey에 normalized/deferred search, tenantSlug, sort, direction 포함
- 검색/정렬 변경 시 cursor를 재시작
- tenant filter option 로딩은 사용자 첫 page 렌더를 막지 않음
## 테스트 기준
RED 테스트는 다음 실패를 먼저 보여야 합니다.
- search/cursor 요청에서 backend가 전체 mirror를 매번 materialize하는 경로가 호출됨
- 첫 page 밖 사용자가 검색되어야 하지만 cursor/search/index 계약이 없어서 전체 scan에 의존함
- page item 50개 매핑 시 tenant lookup 또는 joined tenant lookup이 row 수만큼 반복됨
- Redis mirror stale 상태에서 local DB만으로 정상 목록처럼 응답함
최종 통과 기준:
- Kratos mirror 기준 identity result와 API `items`가 동일합니다.
- Redis index cache hit/miss가 같은 결과를 냅니다.
- Redis stale/failed 상태가 숨겨지지 않습니다.
- local DB row만 있고 Kratos mirror에 없는 사용자는 일반 admin user list에 identity item으로 나오지 않습니다.
- 3,500건 이상 사용자 데이터에서 첫 화면 1500ms 이하, 검색 500ms 이하를 유지합니다.

View File

@@ -2,7 +2,7 @@
## 목적
외부 연동앱이 계정 세션 없이 M2M 방식으로 Baron SSO의 조직구성을 조회할 수 있게 한다. 조직구성 Ory SSOT에서 웜업한 Redis cache Ory에 저장되지 않거나 조회가 불가능한 Backend read model을 Backend가 조합해 cursor 기반 API로 제공한다. Backend DB나 claim output을 SSOT로 사용하지 않으며, iframe 또는 `postMessage` 계약은 사용하지 않는다.
외부 연동앱이 계정 세션 없이 M2M 방식으로 Baron SSO의 조직구성을 조회할 수 있게 한다. 조직구성과 사용자 멤버 정보는 Ory SSOT에서 웜업한 Redis cache 또는 Ory Admin API fallback을 기준으로 제공한다. Backend DB `users`나 claim output을 SSOT 또는 read model로 사용하지 않으며, iframe 또는 `postMessage` 계약은 사용하지 않는다.
## 인증

View File

@@ -0,0 +1,78 @@
# make dev Vite HMR Policy
`make dev`는 로컬 개발 중 소스 변경이 즉시 화면에 반영되는 환경이어야 합니다. `adminfront`, `devfront`, `orgfront``make dev`에서 production `dist`를 정적 서빙하지 않고 Vite dev server로 실행합니다.
## Policy
- `make dev`로 실행되는 React frontends는 Vite HMR 모드여야 합니다.
- 대상 서비스는 `adminfront`, `devfront`, `orgfront`입니다.
- 각 서비스는 Docker Compose에서 해당 Dockerfile의 `dev` target으로 빌드해야 합니다.
- 각 서비스의 working directory는 bind-mounted source path인 `/workspace/<app>`이어야 합니다.
- 각 서비스 command는 `npm run dev -- --host 0.0.0.0 --port <port>` 형태여야 합니다.
- Docker bind mount 환경에서 파일 변경 감지가 누락되지 않도록 `DEV_SERVER_WATCH_POLLING=true`가 기본값이어야 합니다.
- production image는 기존처럼 `production` target에서 build output `dist``serve_frontend_prod.mjs`로 정적 서빙할 수 있습니다. 이 정책은 `make dev` runtime에만 적용합니다.
## Current Ports
- AdminFront: `http://localhost:5173`
- DevFront: `http://localhost:5174`
- OrgFront: `http://localhost:5175`
## Required Structure
Each React frontend Dockerfile must keep these stages:
- `deps`: install workspace dependencies.
- `dev`: run Vite dev server from `/workspace/<app>`.
- `build`: run production build.
- `production`: serve built `dist` with the static server.
`docker-compose.yaml` must select `target: dev` for the three React frontends. This prevents `make dev` from accidentally serving stale `/app/dist` output.
## Regression Guard
The policy is checked by:
```sh
test/frontend_dev_bind_mount_policy_test.sh
test/playwright_frontend_runtime_policy_test.sh
```
The test verifies that:
- source directories are bind-mounted into `/workspace/<app>`;
- `node_modules` stays in container volumes;
- each React frontend uses `target: dev`;
- each React frontend runs `npm run dev`;
- each React frontend enables polling watch by default;
- `serve_frontend_prod.mjs` is not used by the Compose dev service;
- Dockerfiles keep separate `dev`, `build`, and `production` stages.
If this test fails, do not work around it by refreshing the browser or rebuilding manually. Fix the Compose/Dockerfile runtime so `make dev` remains a true HMR development mode.
## Playwright Runtime Policy
Local Playwright tests should use Vite dev servers by default. This keeps local E2E feedback aligned with `make dev` and avoids testing stale production `dist` output while actively editing frontend code.
Gitea Actions and other CI runs should use build output through Vite preview. This validates production bundle behavior before merge.
Required behavior:
- Local default:
- AdminFront uses Vite dev server on port `5173`.
- DevFront uses Vite dev server on port `5174`.
- OrgFront uses Vite dev server on port `5175`.
- CI or explicit preview mode:
- `CI=true` or `PLAYWRIGHT_USE_PREVIEW=true` switches React frontend Playwright web servers to `build` + `preview`.
- Existing running server mode:
- `BASE_URL` can be used for AdminFront and OrgFront.
- `PLAYWRIGHT_BASE_URL` or `BASE_URL` can be used for DevFront.
- `PLAYWRIGHT_SKIP_WEBSERVER=true` disables DevFront's managed web server.
Use explicit preview locally only when testing production bundle behavior:
```sh
PLAYWRIGHT_USE_PREVIEW=true npm --prefix adminfront test
PLAYWRIGHT_USE_PREVIEW=true npm --prefix devfront test
PLAYWRIGHT_USE_PREVIEW=true npm --prefix orgfront test
```

View File

@@ -0,0 +1,38 @@
# Worksmobile Sync Policy
## 겸직 소속 비교 정책
Baron과 WORKS 모두 한 사용자가 여러 조직에 동시에 소속될 수 있다. 따라서 사용자 소속 비교는 배열 순서나 대표 조직 우선순위만으로 불일치를 판단하지 않는다.
### Membership Set 우선
사용자 소속 비교의 기본 단위는 `domainID + orgUnitID`로 구성한 membership set이다.
- Baron expected organization과 WORKS remote organization의 membership set이 같으면 같은 소속으로 본다.
- 같은 membership set 안에서 `organization.primary` 또는 `orgUnit.primary` 우선순위만 다른 경우는 보정 사유가 아니다.
- GPDTDC와 본소속이 모두 있는 사용자도 같은 규칙을 적용한다.
### 보정 대상
다음 차이는 계속 보정 대상이다.
- Baron에는 있는데 WORKS에 없는 orgUnit membership
- WORKS에는 있는데 Baron에는 없는 orgUnit membership
- 비교 대상 position 값 차이
- 비교 대상 manager 값 차이
### 보정 제외
다음 차이는 사용자 `organization` update reason으로 만들지 않는다.
- 같은 membership set에서 GPDTDC와 본소속의 primary 우선순위만 다른 경우
- 같은 membership set에서 WORKS의 조직 배열 순서만 다른 경우
## Grade 비교 정책
직급(`grade`) 차이는 WORKS 사용자 보정 대상이다. 단, 비교 기준은 사용자 전역 `user.grade`가 아니라 테넌트 소속 정보에 연결된 `additionalAppointments[].grade`이다.
- Baron의 테넌트 소속별 `grade`는 같은 WORKS `orgUnit` membership이 속한 organization level과 비교한다.
- 같은 membership에서 Baron 테넌트 소속 `grade`와 WORKS organization level이 다르거나 WORKS level이 비어 있으면 `grade` update reason으로 본다.
- GPDTDC 산하 테넌트의 연구원 직급과 그 외 테넌트의 일반 직급은 직급체계가 다르므로 서로 교차 비교하지 않는다.
- GPDTDC와 본소속을 모두 가진 사용자는 각 membership의 테넌트 소속 `grade`와 해당 WORKS organization level만 각각 비교한다.