forked from baron/baron-sso
chore: snapshot local state before dev merge
This commit is contained in:
@@ -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보다 우선하는 근거로 사용하지 않습니다.
|
||||
|
||||
110
docs/admin-user-list-ssot-cache-and-local-user-db.md
Normal file
110
docs/admin-user-list-ssot-cache-and-local-user-db.md
Normal 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 이하를 유지합니다.
|
||||
@@ -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` 계약은 사용하지 않는다.
|
||||
|
||||
## 인증
|
||||
|
||||
|
||||
78
docs/make-dev-vite-hmr-policy.md
Normal file
78
docs/make-dev-vite-hmr-policy.md
Normal 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
|
||||
```
|
||||
38
docs/worksmobile-sync-policy.md
Normal file
38
docs/worksmobile-sync-policy.md
Normal 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만 각각 비교한다.
|
||||
Reference in New Issue
Block a user