forked from baron/baron-sso
merge upstream
This commit is contained in:
@@ -1,44 +1,54 @@
|
||||
# Baron SSO Data SoT (Source of Truth) Architecture Policy
|
||||
# Baron SSO Data SoT Architecture Policy
|
||||
|
||||
## 1. Core Principle: "Ory Stack is the Single Source of Truth"
|
||||
Baron SSO 시스템에서 인증(Identity), 인가(Authorization), OAuth2 위임(Delegation)의 데이터 원천은 **Ory Stack (Kratos, Keto, Hydra)** 입니다.
|
||||
Backend의 로컬 데이터베이스(PostgreSQL)는 성능 최적화, 검색, 감사(Audit), 비즈니스 메타데이터 관리를 위한 **Read-Model** 및 **Cold Storage**의 역할만 수행합니다.
|
||||
## 1. Core Principle: Ory Stack is the Single Source of Truth
|
||||
|
||||
Baron SSO에서 인증 identity, 권한 관계, OAuth/OIDC 위임의 원장은 Ory Stack입니다.
|
||||
|
||||
- Identity/profile 인증 원장: Ory Kratos
|
||||
- Authorization/ReBAC 원장: Ory Keto
|
||||
- OAuth/OIDC client, consent, token state 원장: Ory Hydra
|
||||
|
||||
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 복제본을 원장처럼 직접 소비하지 않습니다.
|
||||
|
||||
## 2. Component Policies
|
||||
|
||||
### 2.1 Identity & User Profile (Ory Kratos)
|
||||
* **SoT:** Ory Kratos Identity (`traits`, `metadata_public`)
|
||||
* **Local DB (`users` Table):** **Read-Model & Search Index**
|
||||
* **목적:** 대규모 사용자 목록의 고속 검색(`LIKE`), 필터링, 정렬, 테넌트 조인(Join) 지원.
|
||||
* **동기화 전략:** `Async Write-Behind`
|
||||
* 사용자 생성/수정 API는 Kratos 처리가 성공하면 즉시 성공 응답을 반환합니다.
|
||||
* 로컬 DB 동기화는 별도 고루틴(Goroutine)에서 비동기로 수행됩니다.
|
||||
* **장애 격리:** 로컬 DB 장애가 사용자의 로그인/가입 프로세스를 차단하지 않습니다.
|
||||
### 2.1 Identity & User Profile
|
||||
|
||||
### 2.2 Permissions & Relationships (Ory Keto)
|
||||
* **SoT:** Ory Keto (Relation Tuples)
|
||||
* **Local DB:**
|
||||
* 권한 판단 로직을 로컬 DB에 저장하지 않습니다.
|
||||
* `Tenant`, `TenantGroup` 등 비즈니스 객체의 **생성/삭제 이벤트**를 Keto의 관계(Relation)로 비동기 동기화합니다.
|
||||
* 모든 권한 검증(`CheckPermission`)은 반드시 Keto API를 통해 실시간으로 수행합니다.
|
||||
- Ory Kratos identity가 subject, credentials, recovery/verification address, 인증 식별자의 원장입니다.
|
||||
- Kratos identity 변경은 Backend의 중앙 `IdentityWriteService`를 경유해야 합니다.
|
||||
- Redis identity mirror는 빠른 단건/목록/검색 조회를 위한 cache입니다. stale 가능성을 API 응답에 드러내야 합니다.
|
||||
- Backend DB `users`는 사용자 identity/profile/소속 조회 read model이 아닙니다. 남은 의존은 제거 대상이며, 조회 API는 Kratos identity mirror 또는 Kratos Admin API fallback을 기준으로 해야 합니다.
|
||||
|
||||
### 2.3 OAuth2 Clients & Sessions (Ory Hydra)
|
||||
* **SoT:** Ory Hydra (OAuth2 Clients, Access/Refresh Tokens, Consent Sessions)
|
||||
* **Local DB (`client_secrets`, `client_consents`):** **Backup & Query-Model**
|
||||
* `client_secrets`: Hydra는 해시된 시크릿만 저장하므로, 시크릿 재발급 및 관리를 위한 **원본 보관소(Cold Storage)**로 사용합니다.
|
||||
* `client_consents`: Hydra API는 "특정 사용자의 동의 내역" 조회만 지원하므로, "특정 클라이언트의 전체 사용자 동의 목록"을 제공하기 위한 **조회용 모델(Query-Model)**로 사용합니다.
|
||||
### 2.2 Permissions & Relationships
|
||||
|
||||
- 권한 판단과 관계 tuple의 원장은 Ory Keto입니다.
|
||||
- Backend DB는 relation command outbox, 처리 상태, 조직 표시/검색에 필요한 read model을 보관할 수 있습니다.
|
||||
- 보안상 중요한 권한 판정은 Backend DB metadata나 token claim만으로 수행하지 않고 Keto check를 거쳐야 합니다.
|
||||
|
||||
### 2.3 OAuth2 Clients & Sessions
|
||||
|
||||
- OAuth2 client, consent, token state의 프로토콜 원장은 Ory Hydra입니다.
|
||||
- `client_consents` 같은 Backend read model은 Hydra가 제공하지 않는 조회 축을 보완하기 위한 모델입니다.
|
||||
- client secret 원문처럼 Hydra가 해시만 보관하는 값은 재발급/운영 목적의 별도 보관 정책과 감사 로그를 가져야 합니다.
|
||||
|
||||
## 3. Data Flow & Synchronization Strategy
|
||||
|
||||
### 3.1 Write Path (Command)
|
||||
1. **Request:** 클라이언트가 Backend API 요청.
|
||||
2. **Ory Exec:** Backend가 Ory 서비스(Kratos/Hydra/Keto) API를 동기(Synchronous) 호출.
|
||||
3. **Response:** Ory 성공 시 클라이언트에게 즉시 성공 응답 반환 (SoT 확정).
|
||||
4. **Sync:** Backend가 비동기(Goroutine)로 로컬 DB 테이블을 갱신.
|
||||
### 3.1 Write Path
|
||||
|
||||
### 3.2 Read Path (Query)
|
||||
* **Self Context (내 정보, 내 권한):** Ory Session/Token을 통해 직접 검증하거나 Kratos/Keto를 실시간 조회 (Always Fresh).
|
||||
* **Admin Context (목록 조회, 검색):** 로컬 DB를 조회하여 빠른 응답 제공 (Eventually Consistent).
|
||||
1. 클라이언트 또는 운영 도구가 Backend API/CLI를 호출합니다.
|
||||
2. Backend가 중앙 service를 통해 Ory API를 동기 호출합니다.
|
||||
3. Ory write 성공 후 Ory ID로 재조회합니다.
|
||||
4. Redis mirror를 갱신하거나 갱신 실패 시 `stale`/`failed` 상태를 기록합니다.
|
||||
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 identity mirror 또는 Ory Admin API fallback을 기준으로 cursor 기반 API를 제공합니다.
|
||||
- API response는 `identityTotal`, read model count, mirror status를 구분해야 합니다.
|
||||
|
||||
### 3.3 Conflict Resolution
|
||||
* 데이터 불일치가 발견될 경우, 항상 **Ory Stack의 데이터를 기준(Authority)**으로 로컬 DB를 보정(Self-healing)합니다.
|
||||
|
||||
불일치가 발견되면 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 이하를 유지합니다.
|
||||
49
docs/adminfront-flicker-trace-analysis-2026-06-15.md
Normal file
49
docs/adminfront-flicker-trace-analysis-2026-06-15.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# adminfront 깜빡임 trace 분석
|
||||
|
||||
작성일: 2026-06-15
|
||||
|
||||
## 입력 자료
|
||||
|
||||
- `adminfront/Trace-20260615T113806.json.gz`
|
||||
|
||||
## 관찰 결과
|
||||
|
||||
- trace 구간은 약 4.18초이다.
|
||||
- DevTools screenshot은 250장 포함되어 있다.
|
||||
- 화면 전체 shell이 사라지는 현상은 아니고, 본문 영역이 반복적으로 repaint되는 형태이다.
|
||||
- `Animation` 이벤트에서 `enter` 애니메이션이 147회 반복 시작되었다.
|
||||
- 반복 대상은 다음 nodeName으로 확인되었다.
|
||||
- `DIV class='space-y-4 animate-in fade-in duration-500'`
|
||||
- 같은 nodeName의 `Paint` 이벤트도 147회 발생했고, span은 약 4.1초였다.
|
||||
|
||||
## 원인
|
||||
|
||||
장기 유지되는 admin page/tab container에 `animate-in fade-in duration-500` 진입 애니메이션이 붙어 있었다.
|
||||
|
||||
이 클래스가 query/refetch, tab content 갱신, 렌더 상태 변화와 결합되면서 본문 영역이 반복 진입 애니메이션을 수행했고, 사용자는 이를 간헐적인 깜빡임으로 보게 된다.
|
||||
|
||||
## 수정
|
||||
|
||||
다음 장기 컨테이너에서 페이지 레벨 진입 애니메이션을 제거했다.
|
||||
|
||||
- `TenantWorksmobilePage` tab panels
|
||||
- `GlobalOverviewPage` root container
|
||||
- `DataIntegrityPage` tab panels
|
||||
- `TenantDetailPage` nested outlet wrapper
|
||||
|
||||
Dialog, dropdown, toast처럼 짧게 열리고 닫히는 transient UI의 state-based animation은 유지한다.
|
||||
|
||||
## 회귀 방지
|
||||
|
||||
- Worksmobile tab panel에 trace 원인 클래스가 다시 들어오지 않도록 테스트를 추가했다.
|
||||
- adminfront 전체 `.tsx`에서 `animate-in fade-in duration-500` 페이지 레벨 패턴이 재도입되지 않도록 정책 테스트를 추가했다.
|
||||
|
||||
## 검증
|
||||
|
||||
```bash
|
||||
pnpm --dir adminfront exec vitest run src/features/coverage/adminPageAnimationPolicy.test.ts --bail 1
|
||||
pnpm --dir adminfront exec vitest run src/features/tenants/routes/TenantWorksmobilePage.test.ts --bail 1
|
||||
pnpm --dir adminfront exec vitest run src/features/overview/GlobalOverviewPage.test.tsx --bail 1
|
||||
pnpm --dir adminfront exec vitest run src/features/integrity/DataIntegrityPage.test.tsx --bail 1
|
||||
pnpm --dir adminfront exec vitest run src/features/tenants/routes/TenantDetailPage.worksmobile.test.tsx --bail 1
|
||||
```
|
||||
181
docs/adminfront-tab-level-direct-permission-design.md
Normal file
181
docs/adminfront-tab-level-direct-permission-design.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# [RFC/Design] adminfront: 각 탭별 ReBAC 기반 세부 권한 직접 부여 기능 설계
|
||||
|
||||
## 1. 배경 및 목적
|
||||
|
||||
현재 `adminfront` 테넌트 상세 페이지는 대략적인 역할 기반 제어(Coarse-grained RBAC/ReBAC) 형태로만 동작합니다.
|
||||
운영자는 사용자를 **"소유자(Owner)"** 또는 **"테넌트 관리자(Admin)"**로만 임명할 수 있으며, 이 역할에 의해 테넌트 하위의 4개 탭(프로필, 권한 관리, 조직 관리, 사용자 스키마)의 읽기/쓰기 권한이 통째로 결정됩니다.
|
||||
|
||||
하지만 더욱 세밀한 운영 권한 관리가 필요하다는 비즈니스 요구사항에 따라, **"사용자 A에게는 조직 관리 및 스키마 읽기 권한만 부여"**, **"사용자 B에게는 스키마 수정 권한만 부여"**와 같이 탭 레벨에서 세분화된(Fine-grained) 권한을 직접 지정할 수 있는 기능을 신설합니다.
|
||||
|
||||
이 설계는 `devfront`에서 이슈 #1029를 통해 구현 완료한 **"RP 세부 관계 직접 부여"** 철학과 완벽히 동일하며, Ory Keto(ReBAC) 및 아웃박스 정합성 엔진을 관통하여 설계됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 세부 설계 사양
|
||||
|
||||
### 2.1 Ory Keto OPL 스키마 변경 (`docker/ory/keto/namespaces.ts`)
|
||||
|
||||
`Tenant` 네임스페이스 하위에 각 탭별 읽기(`_viewers`)와 쓰기(`_managers`)를 결정하는 **물리적인 직접 관계(Direct Relations)**를 추가합니다.
|
||||
기존 `members`, `admins`, `owners`에 의한 상속 허가 식(Permits)을 유지하여 하위 호환성 및 기존 관리체계의 안정성을 완벽히 보장합니다.
|
||||
|
||||
```typescript
|
||||
class Tenant implements Namespace {
|
||||
related: {
|
||||
owners: (User | SubjectSet<System, "super_admins">)[]
|
||||
admins: (User | SubjectSet<System, "super_admins">)[]
|
||||
members: (User | SubjectSet<System, "super_admins"> | SubjectSet<Tenant, "admins"> | SubjectSet<Tenant, "owners">)[]
|
||||
parents: Tenant[]
|
||||
developer_console_viewer: (User | SubjectSet<System, "super_admins">)[]
|
||||
developer_console_grant_manager: (User | SubjectSet<System, "super_admins">)[]
|
||||
|
||||
// 🌟 신규 직접 관계 (Direct Relations) 정의
|
||||
profile_viewers: (User | SubjectSet<System, "super_admins">)[]
|
||||
profile_managers: (User | SubjectSet<System, "super_admins">)[]
|
||||
|
||||
permissions_viewers: (User | SubjectSet<System, "super_admins">)[]
|
||||
permissions_managers: (User | SubjectSet<System, "super_admins">)[]
|
||||
|
||||
organization_viewers: (User | SubjectSet<System, "super_admins">)[]
|
||||
organization_managers: (User | SubjectSet<System, "super_admins">)[]
|
||||
|
||||
schema_viewers: (User | SubjectSet<System, "super_admins">)[]
|
||||
schema_managers: (User | SubjectSet<System, "super_admins">)[]
|
||||
}
|
||||
|
||||
permits = {
|
||||
// 1. 프로필 (Profile) 탭 허가 규칙
|
||||
view_profile: (ctx: Context): boolean =>
|
||||
this.related.profile_viewers.includes(ctx.subject) ||
|
||||
this.permits.manage_profile(ctx) ||
|
||||
this.permits.view(ctx), // 멤버/관리자/소유자는 기본 조회 가능
|
||||
|
||||
manage_profile: (ctx: Context): boolean =>
|
||||
this.related.profile_managers.includes(ctx.subject) ||
|
||||
this.permits.manage(ctx), // 관리자/소유자는 기본 수정 가능
|
||||
|
||||
// 2. 권한 관리 (Permissions) 탭 허가 규칙
|
||||
view_permissions: (ctx: Context): boolean =>
|
||||
this.related.permissions_viewers.includes(ctx.subject) ||
|
||||
this.permits.manage_permissions(ctx) ||
|
||||
this.permits.view(ctx),
|
||||
|
||||
manage_permissions: (ctx: Context): boolean =>
|
||||
this.related.permissions_managers.includes(ctx.subject) ||
|
||||
this.permits.manage_admins(ctx), // 소유자는 기본 관리 가능
|
||||
|
||||
// 3. 조직 관리 (Organization) 탭 허가 규칙
|
||||
view_organization: (ctx: Context): boolean =>
|
||||
this.related.organization_viewers.includes(ctx.subject) ||
|
||||
this.permits.manage_organization(ctx) ||
|
||||
this.permits.view(ctx),
|
||||
|
||||
manage_organization: (ctx: Context): boolean =>
|
||||
this.related.organization_managers.includes(ctx.subject) ||
|
||||
this.permits.manage(ctx),
|
||||
|
||||
// 4. 사용자 스키마 (Schema) 탭 허가 규칙
|
||||
view_schema: (ctx: Context): boolean =>
|
||||
this.related.schema_viewers.includes(ctx.subject) ||
|
||||
this.permits.manage_schema(ctx) ||
|
||||
this.permits.view(ctx),
|
||||
|
||||
manage_schema: (ctx: Context): boolean =>
|
||||
this.related.schema_managers.includes(ctx.subject) ||
|
||||
this.permits.manage(ctx),
|
||||
|
||||
// --- 기존 마스터 및 상속 규칙 보존 ---
|
||||
view: (ctx: Context): boolean =>
|
||||
this.related.members.includes(ctx.subject) ||
|
||||
this.related.admins.includes(ctx.subject) ||
|
||||
this.related.owners.includes(ctx.subject) ||
|
||||
this.related.parents.traverse((p) => p.permits.view(ctx)),
|
||||
|
||||
manage: (ctx: Context): boolean =>
|
||||
this.related.admins.includes(ctx.subject) ||
|
||||
this.related.owners.includes(ctx.subject) ||
|
||||
this.related.parents.traverse((p) => p.permits.manage(ctx)),
|
||||
|
||||
manage_admins: (ctx: Context): boolean =>
|
||||
this.related.owners.includes(ctx.subject) ||
|
||||
this.related.parents.traverse((p) => p.permits.manage_admins(ctx))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 백엔드 API 설계 (`backend/internal/handler/tenant_handler.go`)
|
||||
|
||||
세부 권한 부여/회수 API는 해당 테넌트의 최상위 권한 관리자만 수행할 수 있도록 **`Tenant#manage_admins`** 허가 규칙으로 강력하게 인가 보호합니다.
|
||||
|
||||
#### A. 세부 권한 관계 전체 조회 API
|
||||
* **Endpoint**: `GET /api/v1/admin/tenants/:id/relations`
|
||||
* **인가 필터**: `RequireKetoPermission(config, "Tenant", "manage_admins")`
|
||||
* **반환 DTO**:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"userId": "00000000-0000-0000-0000-000000000010",
|
||||
"name": "홍길동",
|
||||
"email": "kildong@hmac.kr",
|
||||
"relations": ["profile_managers", "schema_viewers"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### B. 세부 권한 관계 부여 API
|
||||
* **Endpoint**: `POST /api/v1/admin/tenants/:id/relations`
|
||||
* **인가 필터**: `RequireKetoPermission(config, "Tenant", "manage_admins")`
|
||||
* **Payload**:
|
||||
```json
|
||||
{
|
||||
"userId": "00000000-0000-0000-0000-000000000010",
|
||||
"relation": "profile_managers"
|
||||
}
|
||||
```
|
||||
* **동작**: 트랜잭셔널 아웃박스에 적재하여 Keto에 `Tenant:<ID>#profile_managers@User:<UserID>` 튜플 반영.
|
||||
|
||||
#### C. 세부 권한 관계 회수 API
|
||||
* **Endpoint**: `DELETE /api/v1/admin/tenants/:id/relations`
|
||||
* **인가 필터**: `RequireKetoPermission(config, "Tenant", "manage_admins")`
|
||||
* **Payload**:
|
||||
```json
|
||||
{
|
||||
"userId": "00000000-0000-0000-0000-000000000010",
|
||||
"relation": "profile_managers"
|
||||
}
|
||||
```
|
||||
* **동작**: 트랜잭셔널 아웃박스에 적재하여 Keto 내 튜플 삭제 반영.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 프론트엔드 UI 설계
|
||||
|
||||
사용자에게 역할(Role) 외에 세부적인 설정을 직관적으로 관리할 수 있도록, 기존 **"권한 관리"** 탭 하단에 **"세부 권한 설정 (Fine-grained Permissions)"** 섹션을 신설합니다.
|
||||
|
||||
#### A. 구성 요소
|
||||
1. **유저 검색/추가 패널**: 테넌트 소속 사용자를 검색하여 격리 설정 테이블(Matrix)에 추가합니다.
|
||||
2. **세부 권한 격리 매트릭스 (Matrix Table)**:
|
||||
* 컬럼: `이름` | `이메일` | `테넌트 프로필` | `권한 관리` | `조직 관리` | `사용자 스키마` | `작업`
|
||||
* 각 탭 컬럼은 드롭다운 셀렉트 박스로 채워집니다:
|
||||
* **`권한 없음 (None)`** / **`조회 가능 (Read)`** / **`수정 가능 (Write)`**
|
||||
3. **상태 동기화 연동**:
|
||||
* 셀렉트 박스에서 `조회 가능(Read)` 선택 시: `_viewers` 관계 추가(`POST`) & `_managers` 관계 회수(`DELETE`).
|
||||
* 셀렉트 박스에서 `수정 가능(Write)` 선택 시: `_managers` 관계 추가(`POST`) & `_viewers` 관계 회수(`DELETE`).
|
||||
* 셀렉트 박스에서 `권한 없음(None)` 선택 시: 둘 다 회수(`DELETE`).
|
||||
|
||||
---
|
||||
|
||||
## 3. 작업 계획 및 테스트 전략
|
||||
|
||||
1. **OPL 컴파일 및 빌드 검증**:
|
||||
* namespaces.ts 수정 후 Keto OPL 테스트를 구동하여 컴파일 문법에 문제가 없는지 사전 검증합니다.
|
||||
2. **백엔드 구현 및 DB 연동**:
|
||||
* `tenant_handler.go`에 신규 핸들러 추가 후 gg/gorm 아웃박스 통합을 완료합니다.
|
||||
3. **프론트엔드 연동 및 Matrix UI 개발**:
|
||||
* `TenantAdminsAndOwnersTab.tsx` 하단부 카드에 매트릭스 테이블 영역을 추가합니다.
|
||||
4. **유형 및 단위 테스트**:
|
||||
* 신설된 REST API 명세를 테스트하는 고성능 백엔드 단위 테스트를 작성합니다.
|
||||
* 프론트엔드에서 체크박스 변경 시 올바른 릴레이션이 트리거되는지 검증하는 Vitest 렌더 테스트를 작성합니다.
|
||||
@@ -25,7 +25,7 @@ graph TD
|
||||
G -- Yes --> J[Ory Kratos 계정 생성]
|
||||
|
||||
%% 유저 생성 및 권한 할당
|
||||
J --> K[(Local DB 유저 레코드 생성)]
|
||||
J --> K[(Backend read model 레코드 생성)]
|
||||
K --> N[기본 권한 할당: user<br>Keto: members 부여]
|
||||
|
||||
N --> O([회원가입 완료])
|
||||
|
||||
@@ -85,7 +85,7 @@ baron-sso-backup-YYYYMMDD-HHMMSSZ/
|
||||
|
||||
| 서비스 필터 | 주요 dump 산출물 | 포함 데이터 | 복구 중요도 | 복구 영향도 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `postgres` | `postgres/baron.dump` | Baron users, tenants, membership, user_login_ids, user_groups, RP metadata, API keys, WORKS mapping/outbox, Keto outbox, consent projection 등 | 필수 | Baron control plane의 원장이다. 누락되면 사용자/테넌트/RP/WORKS 참조가 끊기고 Ory DB만 복구해도 서비스 의미가 깨진다. |
|
||||
| `postgres` | `postgres/baron.dump` | Baron users, tenants, membership, user_login_ids, user_groups, RP metadata, API keys, WORKS mapping/outbox, Keto outbox, consent read model 등 | 필수 | Ory에 저장되지 않거나 조회가 불가능한 Baron control plane 데이터와 처리 상태를 담는다. 누락되면 사용자/테넌트/RP/WORKS 참조가 끊기고 Ory DB만 복구해도 서비스 의미가 깨진다. |
|
||||
| `ory-postgres` | `postgres/globals.sql`, `postgres/ory_kratos.dump`, `postgres/ory_hydra.dump`, `postgres/ory_keto.dump` | Kratos identity/credential/session, Hydra client/consent/token state, Keto relation tuple | 필수 | 인증 주체, OAuth2/OIDC 상태, ReBAC 권한 원장이다. Baron DB와 시점이 다르면 로그인/인가/consent 불일치가 발생한다. |
|
||||
| `clickhouse` | `clickhouse/baron_clickhouse/schema/*.sql`, `clickhouse/baron_clickhouse/data/*.native` | Baron audit_logs, RP usage event/aggregate 등 | 운영 정책상 필수 | 인증 자체를 막지는 않지만 감사 추적, 사용량 집계, 사고 분석 이력이 손실된다. |
|
||||
| `ory-clickhouse` | `clickhouse/ory_clickhouse/schema/*.sql`, `clickhouse/ory_clickhouse/data/*.native` | Oathkeeper/Ory/Vector 접근 로그 | 운영 정책상 필수 | Ory edge 접근 로그와 장애 분석 근거가 손실된다. 인증 원장은 Postgres에 있으므로 직접 로그인 기능 영향은 제한적이다. |
|
||||
@@ -287,9 +287,10 @@ Upload flow:
|
||||
2. `dump.sh`가 `reports/backup-report.md`를 생성한다. report에는 사용자 수, 테넌트 수, RP 수, Hydra client 수, WORKS 관련 row count, 서비스별 수행 시간이 Markdown 표로 기록된다.
|
||||
3. `upload_cloud.sh`가 백업 디렉터리를 `baron-sso-backup-*.tar.zst`로 압축한다.
|
||||
4. Drive API용 access token을 확인한다.
|
||||
- 우선순위: `WORKS_DRIVE_ACCESS_TOKEN`, `WORKS_DRIVE_ACCESS_TOKEN_FILE`, `WORKS_DRIVE_ACCESS_TOKEN_CMD`
|
||||
- fallback 1: `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`으로 Drive 앱 access token 갱신
|
||||
- fallback 2: `WORKS_DRIVE_OAUTH_*` 서비스 계정 JWT 토큰 발급
|
||||
- `WORKS_DRIVE_ACCESS_TOKEN`, `WORKS_DRIVE_ACCESS_TOKEN_FILE`, `WORKS_DRIVE_ACCESS_TOKEN_CMD`는 항상 최우선이다.
|
||||
- `WORKS_DRIVE_AUTH_MODE=auto` 기본값은 service account credentials가 완비되어 있으면 `WORKS_DRIVE_OAUTH_*` 서비스 계정 JWT 토큰 발급을 먼저 사용하고, 없으면 `WORKS_DRIVE_OAUTH_REFRESH_TOKEN` 갱신으로 fallback한다.
|
||||
- `WORKS_DRIVE_AUTH_MODE=service-account`는 service account JWT 토큰 발급을 강제한다.
|
||||
- `WORKS_DRIVE_AUTH_MODE=refresh-token`은 service account 설정이 있어도 refresh token 갱신을 강제한다.
|
||||
5. WORKS Drive upload URL 생성 API를 호출한다.
|
||||
6. 발급받은 upload URL에 multipart `Filedata`로 `.tar.zst` archive를 업로드한다.
|
||||
7. `WORKS_DRIVE_UPLOAD_REPORTS=true`이면 대상 폴더 아래 `WORKS_DRIVE_REPORT_FOLDER_NAME` 하위 폴더를 찾거나 생성한 뒤 `reports/*.md`만 업로드한다.
|
||||
@@ -312,8 +313,13 @@ Upload flow:
|
||||
- Drive API는 `file` scope가 필요하다.
|
||||
- `WORKS_DRIVE_PARENT_FILE_ID`는 폴더 이름이나 경로가 아니라 WORKS Drive API가 반환하는 폴더 `fileId`여야 한다.
|
||||
- 계정 동기화용 `WORKS_ADMIN_OAUTH_*`와 백업 업로드용 `WORKS_DRIVE_OAUTH_*`는 용도가 다른 앱/키로 분리한다.
|
||||
- 운영 기본 경로는 Drive용 access token을 명시 주입하거나 `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`으로 갱신하는 방식이다.
|
||||
- 서비스 계정 JWT fallback은 Drive 업로드 앱 정책에서 Drive scope 위임이 허용된 경우에만 성공한다.
|
||||
- 운영 기본 경로는 Drive용 access token을 명시 주입하거나 service account JWT를 사용하는 방식이다.
|
||||
- refresh token을 재발급해 사용하는 경우 `make works-drive-refresh-token`을 사용한다. WORKS OAuth refresh token은 Authorization Code Flow에서 발급되며, WORKS Token 설정의 Refresh Token Rotation 상태에 따라 갱신 응답에 새 refresh token이 포함될 수 있다.
|
||||
- 기존 refresh token이 아직 유효하면 `make works-drive-refresh-token WORKS_DRIVE_TOKEN_GRANT=refresh-token`로 `.env`의 `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`을 자동 갱신한다.
|
||||
- 기존 refresh token이 폐기되어 401이 발생하면 `WORKS_DRIVE_TOKEN_GRANT=print-authorize-url scripts/backup/refresh_works_drive_token.sh`로 출력된 URL을 브라우저에서 승인한 뒤, callback의 `code`를 `make works-drive-refresh-token WORKS_DRIVE_TOKEN_GRANT=authorization-code WORKS_DRIVE_AUTH_CODE=<code>`에 전달한다.
|
||||
- callback URL 전체를 복사할 수 있으면 `WORKS_DRIVE_AUTH_CALLBACK_URL=<callback-url>`을 사용해도 된다.
|
||||
- 토큰 갱신 도구는 짧게 만료되는 access token을 `.env`에 저장하지 않고 refresh token과 `WORKS_DRIVE_AUTH_MODE=refresh-token`만 갱신한다.
|
||||
- 서비스 계정 JWT는 Drive 업로드 앱 정책에서 Drive scope 위임이 허용되고 Client ID, Service Account, Private Key가 같은 앱에서 발급된 조합일 때만 성공한다.
|
||||
- 파일 크기가 WORKS Drive 단일 파일 제한에 걸릴 수 있으면 `WORKS_DRIVE_MAX_SINGLE_FILE_BYTES` 또는 `WORKS_DRIVE_FORCE_SPLIT=true`로 split part 업로드를 사용한다.
|
||||
- Markdown report 업로드 기본 폴더명은 `reports`이며 `WORKS_DRIVE_REPORT_FOLDER_NAME`으로 바꿀 수 있다.
|
||||
- 외부 업로드 성공은 복구 가능성을 보장하지 않는다. 업로드 후에도 `make dump-verify BACKUP=...`와 restore rehearsal을 별도로 수행해야 한다.
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
## 현재 구조
|
||||
|
||||
- Tenant custom schema는 `tenants.config.userSchema` JSONB에 저장한다.
|
||||
- Tenant custom value는 backend DB의 `users.metadata` JSONB에 저장한다.
|
||||
- Tenant custom value는 Ory에 저장되지 않거나 Ory API로 필요한 조회가 불가능한 값에 한해 backend DB의 `users.metadata` JSONB read model에 저장한다.
|
||||
- `isLoginId=true`인 Tenant field 값은 로그인 식별자 처리를 위해 `user_login_ids`에도 동기화한다.
|
||||
- Ory Kratos traits에는 인증/식별에 필요한 최소 값만 동기화하는 방향으로 정리한다.
|
||||
- RP custom value는 backend DB의 `rp_user_metadata.metadata` JSONB에 별도 저장한다.
|
||||
- RP custom value는 Ory에 저장되지 않는 RP 범위 운영 값으로 보고 backend DB의 `rp_user_metadata.metadata` JSONB read model에 별도 저장한다.
|
||||
|
||||
## Tenant Custom Field
|
||||
|
||||
@@ -50,7 +50,7 @@ RP custom schema는 client metadata의 `customUserSchema`에 저장한다.
|
||||
}
|
||||
```
|
||||
|
||||
RP custom value는 `rp_user_metadata` 테이블에 저장한다.
|
||||
RP custom value는 `rp_user_metadata` 테이블에 저장한다. 이 테이블은 Ory SSOT를 대체하지 않는 RP 범위 read model이며, Kratos traits나 token claim output을 원장으로 취급하지 않는다.
|
||||
|
||||
```text
|
||||
client_id text
|
||||
@@ -90,7 +90,7 @@ PUT payload:
|
||||
- GIN 인덱스는 backend index manager가 별도 상태로 관리하는 방향을 원칙으로 한다.
|
||||
- API 요청 처리 중 `CREATE INDEX`를 동기 실행하지 않는다.
|
||||
|
||||
## Claim Projection
|
||||
## Claim Assembly
|
||||
|
||||
JWT 또는 userinfo 응답에서는 custom field를 top-level에 풀지 않는다.
|
||||
Tenant/RP 단위로 묶어서 전달한다.
|
||||
@@ -105,14 +105,6 @@ Tenant/RP 단위로 묶어서 전달한다.
|
||||
"employeeNo": "E1001"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rp_profiles": [
|
||||
{
|
||||
"client_id": "sample-rp",
|
||||
"fields": {
|
||||
"approvalLevel": "A"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -120,9 +112,9 @@ Tenant/RP 단위로 묶어서 전달한다.
|
||||
- `claimEnabled=true` field만 RP claim 후보로 포함한다.
|
||||
- 긴 JSON 값은 기본적으로 token claim보다 userinfo/profile API 응답에 싣는 방향을 우선한다.
|
||||
|
||||
## 한맥가족 Tenant Claim Projection
|
||||
## 한맥가족 Tenant Claim Assembly
|
||||
|
||||
한맥가족(`hanmac-family`) subtree의 tenant claim은 기본 claim과 상세 claim으로 나눈다. 기본 claim은 대표소속 tenant UUID인 `tenant_id`와 전체 소속 목록인 `joined_tenants`이며, RP가 `tenant` claim을 요청하면 tenant별 map 안에 조직 소속 정보를 묶어서 전달한다. 이 정보는 RP가 tenant context를 표시하거나 조직별 기본값을 선택하기 위한 projection이며, 관계형 데이터의 SoT는 PostgreSQL Business DB와 사용자 metadata이다.
|
||||
한맥가족(`hanmac-family`) subtree의 tenant claim은 기본 claim과 상세 claim으로 나눈다. 기본 claim은 대표소속 tenant UUID인 `tenant_id`와 전체 소속 목록인 `joined_tenants`이며, RP가 `tenant` claim을 요청하면 tenant별 map 안에 조직 소속 정보를 묶어서 전달한다. 이 정보는 RP가 tenant context를 표시하거나 조직별 기본값을 선택하기 위한 claim assembly 결과다. 관계/권한 판단은 Ory Keto를 기준으로 하고, Ory에 저장되지 않거나 조회가 불가능한 표시/검색 metadata만 Backend read model에서 보강한다.
|
||||
|
||||
기본 claim 예시는 다음과 같다.
|
||||
|
||||
@@ -235,7 +227,7 @@ Issue #775 구현 결과 기준으로 RP가 `tenant` claim을 요청했을 때
|
||||
- 대표소속 결정은 명시적 `tenant_id`, `additionalAppointments`의 `representative/isPrimary/primary=true`, 가장 먼저 등록된 소속 순서로 적용한다.
|
||||
- 생성 시 소속 tenant가 하나도 없으면 PERSONAL tenant를 자동 생성하고, 해당 tenant를 `tenant_id`와 `joined_tenants`에 포함한다.
|
||||
- RP/client tenant context는 대표소속 `tenant_id`를 덮어쓰지 않는다.
|
||||
- tenant별 namespaced traits map이 없어도 `tenant_id` 또는 `additionalAppointments[].tenantId`를 기준으로 projection 항목을 만들 수 있다.
|
||||
- tenant별 namespaced traits map이 없어도 `tenant_id` 또는 `additionalAppointments[].tenantId`를 기준으로 claim assembly 항목을 만들 수 있다.
|
||||
- 멀티 소속이면 기본 claim의 `joined_tenants`에 모든 소속 tenant를 넣는다. `tenant` claim 요청 시에는 `tenants`에도 모든 소속 tenant 상세를 넣고, `lead_tenants`에는 lead tenant만 넣는다.
|
||||
- token 크기 보호를 위해 전체 조직도나 긴 custom JSON은 claim에 싣지 않고 profile/userinfo API 또는 backend API 응답으로 분리한다.
|
||||
- RP는 `joined_tenants`로 전체 소속을 읽고, `lead_tenants`로 lead tenant를 빠르게 식별한다. 상세 표시는 `tenants[tenant_id]` 또는 `tenants[joined_tenants[n]]`와 `ancestors`를 조합한다.
|
||||
|
||||
@@ -51,7 +51,7 @@ Baron SSO의 신원/권한 SoT는 Ory Stack(Kratos, Keto, Hydra)입니다. 이
|
||||
- `super_admin`은 adminfront 개요 화면 하단에서도 최종 검증 상태, 실패 건수, 검사 시각, 섹션별 상태 요약을 볼 수 있습니다.
|
||||
- `tenant_admin` 등 non-super role은 화면 접근 시 권한 없음 메시지만 봅니다.
|
||||
- 개요 화면의 전체 테넌트 수는 `fetchAllTenants()`로 실제 cursor pagination을 끝까지 수집한 리스트 수를 우선 사용합니다. 이로써 super가 보는 전체 테넌트 수와 리스트 기반 수치가 같은 소스에서 나오도록 맞춥니다.
|
||||
- 개요 화면은 `super_admin`에게 전체 사용자 수(`totalUsers`)도 표시합니다. 이 값은 Kratos user projection 상태의 `projectedUsers` 기준입니다.
|
||||
- 개요 화면은 `super_admin`에게 전체 사용자 수(`totalUsers`)도 표시합니다. 이 값은 Kratos identity mirror 상태의 observed user count 기준입니다.
|
||||
|
||||
## 운영 주의
|
||||
|
||||
|
||||
@@ -64,12 +64,12 @@ sequenceDiagram
|
||||
|
||||
### 자동 등록된 `devfront` 명세
|
||||
```bash
|
||||
hydra clients create
|
||||
--endpoint http://hydra:4445
|
||||
--id devfront
|
||||
--grant-types authorization_code,refresh_token
|
||||
--response-types code
|
||||
--scope openid,offline_access,profile,email
|
||||
hydra clients create
|
||||
--endpoint http://hydra:4445
|
||||
--id devfront
|
||||
--grant-types authorization_code,refresh_token
|
||||
--response-types code
|
||||
--scope openid,profile,email
|
||||
--token-endpoint-auth-method none \ # Public Client (PKCE 사용)
|
||||
--callbacks http://localhost:5174/auth/callback;
|
||||
```
|
||||
|
||||
@@ -140,12 +140,12 @@ scrape_configs:
|
||||
- source_labels: ['container_name']
|
||||
regex: '(baron_.*|oathkeeper|kratos|hydra|keto)'
|
||||
action: keep
|
||||
# 컨테이너 명에서 앞의 접두사를 떼어 서비스 및 잡 라벨 부여 (예: baron_backend -> backend)
|
||||
# 컨테이너 명에서 앞의 접두사를 떼어 서비스 및 잡 라벨 부여 (예: baron_backend -> backend, kratos -> kratos)
|
||||
- source_labels: ['container_name']
|
||||
regex: 'baron_(.*)'
|
||||
regex: '(?:baron_)?(.*)'
|
||||
target_label: 'service'
|
||||
- source_labels: ['container_name']
|
||||
regex: 'baron_(.*)'
|
||||
regex: '(?:baron_)?(.*)'
|
||||
target_label: 'job'
|
||||
# 동적 라벨 추가
|
||||
- target_label: 'app_env'
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
## 결정
|
||||
|
||||
사용자 identity에 대해 PostgreSQL DB projection을 SSOT 일치 보장 대상으로 취급하지 않습니다.
|
||||
사용자 identity에 대해 Backend DB 복제본이나 claim output을 SSOT 일치 보장 대상으로 취급하지 않습니다.
|
||||
|
||||
Baron SSO의 identity 원장은 Ory Kratos입니다. Redis는 Kratos identity를 빠르게 조회하기 위한 mirror/cache 계층이고, PostgreSQL `users`는 Baron 비즈니스 메타데이터, WORKS/Keto/RP 연동 참조, 감사 가능한 로컬 레코드로만 사용합니다.
|
||||
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로 제공합니다.
|
||||
|
||||
## 역할 분리
|
||||
|
||||
@@ -14,13 +16,13 @@ Baron SSO의 identity 원장은 Ory Kratos입니다. Redis는 Kratos identity를
|
||||
| --- | --- | --- |
|
||||
| Kratos `identities` | identity SSOT | 인증 주체, credentials, recovery/verifiable address의 원장 |
|
||||
| Redis identity mirror | cache/read mirror | 빠른 목록/검색/단건 조회. stale 가능 |
|
||||
| PostgreSQL `users` | business local record | tenant, WORKS, RP, Keto 연동에 필요한 Baron 로컬 상태 |
|
||||
| Backend DB read model | Ory 보완 read model | Ory에 저장되지 않거나 조회 불가능한 업무/운영 데이터 |
|
||||
|
||||
PostgreSQL `users`의 visible count를 Kratos identity total로 표시하지 않습니다. 화면과 API에서는 identity mirror count와 local business user count를 분리해서 보여야 합니다.
|
||||
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 업무/조직/연동 정보는 backend DB 전용으로 이동하는 방향을 기준으로 합니다.
|
||||
현재 코드 기준으로 Kratos traits와 backend DB `users`는 일부 필드를 중복 보관합니다. Redis mirror 전환 이후에는 Kratos traits를 인증/기본 identity 필드 중심으로 줄이고, Baron 업무/조직/연동 정보는 Ory에 저장되지 않거나 조회가 불가능한 경우에만 Backend read model로 유지합니다.
|
||||
|
||||
### Kratos에 유지할 identity 필드
|
||||
|
||||
@@ -38,32 +40,32 @@ PostgreSQL `users`의 visible count를 Kratos identity total로 표시하지 않
|
||||
|
||||
| 필드 | 현재 코드 사용 | 전환 방향 |
|
||||
| --- | --- | --- |
|
||||
| `tenant_id` | 대표 테넌트, profile, local user sync | backend `users.tenant_id`와 membership/Keto 기준으로 이동. Kratos에는 최소 claim 캐시로만 허용 |
|
||||
| `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 연동 | backend membership metadata 또는 `users.metadata` 기준 |
|
||||
| `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 DB에만 저장되거나 DB가 원장이어야 하는 정보
|
||||
### Backend read model로만 허용하는 정보
|
||||
|
||||
| 데이터 | 저장 위치 | Kratos에 두지 않는 이유 |
|
||||
| 데이터 | 저장 위치 | Ory SSOT와의 관계 |
|
||||
| --- | --- | --- |
|
||||
| soft-delete 상태 | `users.deleted_at` | Baron 운영/감사 로컬 상태. Kratos identity 삭제/비활성화와 의미가 다름 |
|
||||
| Baron 사용자 상태 세부값 | `users.status` | WORKS provision/deprovision, org visible 정책과 결합된 업무 상태 |
|
||||
| WORKS mapping/outbox/job 상태 | `worksmobile_*` 테이블 | 외부 SaaS 연동 상태이며 identity 인증 원장이 아님 |
|
||||
| Keto outbox 및 relation sync 상태 | `keto_outboxes`, Keto | 권한/관계 원장과 처리 상태 |
|
||||
| RP metadata/consent/usage | `rp_user_metadata`, `client_consents`, usage tables | RP별 업무 데이터와 위임 상태 |
|
||||
| tenant tree, slug, visibility, owner/admin | `tenants`, relation/outbox | 조직/권한 원장. Kratos traits에 넣으면 stale claim이 됨 |
|
||||
| custom field schema 및 tenant별 값 | tenant config, `users.metadata`, related tables | 조회/검색/검증 정책이 tenant별로 달라짐 |
|
||||
| `user_login_ids` row metadata | `user_login_ids` | Kratos는 identifier 값만 필요. 발급 tenant/field key는 backend 업무 정보 |
|
||||
| audit/session activity projection | audit/clickhouse/local tables | 감사/운영 분석 데이터 |
|
||||
| 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에 필요한 데이터는 backend DB가 맡습니다.
|
||||
정리하면, Kratos에는 “로그인과 subject 확인에 필요한 최소 identity”만 남깁니다. 조직도/WORKS/RP/Keto/감사/tenant custom schema에 필요한 데이터도 Ory에 저장되거나 조회 가능한 경우에는 Ory를 기준으로 하고, 그렇지 않은 영역만 Backend read model을 허용합니다.
|
||||
|
||||
## 일관성 모델
|
||||
|
||||
@@ -87,6 +89,7 @@ Kratos Admin API를 backend 밖에서 직접 수정하는 경로는 운영 정
|
||||
|
||||
| 파일 | 역할 | 판정 |
|
||||
| --- | --- | --- |
|
||||
| `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`로 이동 |
|
||||
|
||||
@@ -100,7 +103,7 @@ Kratos Admin API를 backend 밖에서 직접 수정하는 경로는 운영 정
|
||||
| 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` | 허용이지만 local DB sync가 goroutine 기반이라 write-through 기준에서는 약함 |
|
||||
| 일반 회원가입 | `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 기준 |
|
||||
@@ -112,6 +115,8 @@ Kratos Admin API를 backend 밖에서 직접 수정하는 경로는 운영 정
|
||||
| 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를 모두 우회하는 경로
|
||||
|
||||
@@ -127,7 +132,7 @@ Kratos Admin API를 backend 밖에서 직접 수정하는 경로는 운영 정
|
||||
- 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를 직접 수정한 경우에는 PostgreSQL projection이나 Redis mirror를 신뢰하지 않고, Kratos full refresh와 drift report를 먼저 실행합니다.
|
||||
- shell/SQL로 Kratos DB를 직접 수정한 경우에는 Backend read model이나 Redis mirror를 신뢰하지 않고, Kratos full refresh와 drift report를 먼저 실행합니다.
|
||||
- CI에 정적 정책 테스트를 추가해 `admin/identities` write 호출과 `UPDATE identities` SQL이 허용 파일 밖에 생기면 실패시킵니다.
|
||||
|
||||
## Redis 키 설계
|
||||
@@ -155,7 +160,7 @@ Kratos Admin API를 backend 밖에서 직접 수정하는 경로는 운영 정
|
||||
- `identityTotal`: Redis mirror 기준 Kratos identity 수
|
||||
- `localUserTotal`: PostgreSQL `users` 기준 Baron 로컬 사용자 수
|
||||
- `mirrorStatus`: Redis mirror 상태
|
||||
- `items`: identity mirror와 local business metadata를 조합한 응답
|
||||
- `items`: identity mirror와 허용된 Backend read model을 조합한 응답
|
||||
|
||||
Redis cache miss 발생 시:
|
||||
|
||||
@@ -163,11 +168,11 @@ Redis cache miss 발생 시:
|
||||
2. fallback 성공 시 Redis mirror를 갱신합니다.
|
||||
3. fallback 실패 시 SSOT 조회 실패로 응답합니다.
|
||||
|
||||
목록 조회는 Redis mirror가 `ready`가 아니면 경고 상태를 포함해야 합니다. DB projection을 대체 SSOT처럼 사용하지 않습니다.
|
||||
목록 조회는 Redis mirror가 `ready`가 아니면 경고 상태를 포함해야 합니다. Backend read model을 대체 SSOT처럼 사용하지 않습니다.
|
||||
|
||||
## Front 전송과 cursor 보장
|
||||
|
||||
front로 전달되는 사용자 목록은 cursor 기반을 원칙으로 합니다. offset은 하위 호환 파라미터로만 유지하고, 신규 화면 또는 대량 조회 화면은 cursor 외 방식을 사용하지 않습니다.
|
||||
front/API로 전달되는 사용자 목록은 Backend가 제공하는 cursor 기반을 원칙으로 합니다. offset은 하위 호환 파라미터로만 유지하고, 신규 화면 또는 대량 조회 화면은 cursor 외 방식을 사용하지 않습니다.
|
||||
|
||||
### API 계약
|
||||
|
||||
@@ -206,8 +211,8 @@ front로 전달되는 사용자 목록은 cursor 기반을 원칙으로 합니
|
||||
| `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` 조직도 | `fetchUsers(5000, 0)` 단일 호출 | cursor 기반 전체 수집 helper로 전환 |
|
||||
| `orgfront` 조직 picker | `fetchUsers(5000, 0)` 단일 호출 | cursor 기반 전체 수집 helper로 전환 |
|
||||
| `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 원칙:
|
||||
@@ -232,14 +237,15 @@ refresh 중 불일치 또는 실패가 발생하면:
|
||||
## 금지 사항
|
||||
|
||||
- Kratos partial list를 full snapshot으로 간주하지 않습니다.
|
||||
- PostgreSQL `users`를 Kratos identity total의 원장으로 사용하지 않습니다.
|
||||
- Backend read model을 Kratos identity total의 원장으로 사용하지 않습니다.
|
||||
- Redis mirror refresh 실패를 숨기고 `ready`로 표시하지 않습니다.
|
||||
- 외부 도구가 Kratos Admin API를 직접 수정하도록 허용하지 않습니다.
|
||||
|
||||
## 전환 작업
|
||||
|
||||
1. `user_projection` 명칭과 API를 `identity_mirror` 성격으로 분리합니다.
|
||||
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. 기존 DB projection 기반 count를 사용하는 화면과 WORKS 비교 경로를 점검합니다.
|
||||
5. 기존 Backend DB count를 identity count처럼 사용하는 화면과 WORKS 비교 경로를 점검합니다.
|
||||
6. Kratos identity 변경은 `IdentityWriteService` 경유를 강제하고, 직접 `KratosAdmin.UpdateIdentity` 경로를 정책 테스트로 차단합니다.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 목적
|
||||
|
||||
외부 연동앱이 계정 세션 없이 M2M 방식으로 Baron SSO의 조직구성을 조회할 수 있게 한다. 조직구성은 Baron SSO backend의 tenant/user projection을 SSOT로 사용하며, iframe 또는 `postMessage` 계약은 사용하지 않는다.
|
||||
외부 연동앱이 계정 세션 없이 M2M 방식으로 Baron SSO의 조직구성을 조회할 수 있게 한다. 조직구성과 사용자 멤버 정보는 Ory SSOT에서 웜업한 Redis cache 또는 Ory Admin API fallback을 기준으로 제공한다. Backend DB `users`나 claim output을 SSOT 또는 read model로 사용하지 않으며, iframe 또는 `postMessage` 계약은 사용하지 않는다.
|
||||
|
||||
## 인증
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
schema 추가 검토 후보:
|
||||
|
||||
- backend projection에서 읽는 `position`, `jobTitle`
|
||||
- Backend read model에서 읽는 `position`, `jobTitle`
|
||||
- 한맥가족 다중 소속을 metadata로 유지할 경우 `additionalAppointments`
|
||||
- 대표 테넌트 표시값을 traits로 계속 줄 경우 `primaryTenantId`, `primaryTenantSlug`, `primaryTenantName`, `primaryTenantIsOwner`
|
||||
|
||||
@@ -78,5 +78,5 @@ schema 추가 검토 후보:
|
||||
|
||||
1. Personal 사용자는 사용자별 Personal 테넌트를 생성하지 않고 전역 `personal` 테넌트만 사용합니다.
|
||||
2. Kratos traits는 인증/클레임에 필요한 최소 필드만 유지합니다.
|
||||
3. 조직도나 연동 전용 확장 데이터는 traits 최상위에 흩뿌리지 않고 Baron DB의 user projection 또는 명시된 metadata 구조로 모읍니다.
|
||||
3. 조직도나 연동 전용 확장 데이터는 traits 최상위에 흩뿌리지 않고 Ory에 저장되지 않거나 조회가 불가능한 Backend read model 또는 명시된 metadata 구조로 모읍니다.
|
||||
4. `additionalProperties: true`를 바로 `false`로 바꾸면 기존 identity 갱신이 실패할 수 있으므로, 먼저 backend sanitizer와 마이그레이션으로 제거 후보를 정리한 뒤 schema를 닫습니다.
|
||||
|
||||
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
|
||||
```
|
||||
@@ -157,7 +157,7 @@ Baron은 기본적으로 대표소속 tenant와 전체 소속 tenant 목록을
|
||||
|
||||
주의사항:
|
||||
|
||||
- Tenant tree, 직급, 직무, 직책은 PostgreSQL Business SoT와 tenant/user metadata를 기준으로 합니다. Kratos traits는 인증 식별 정보 중심으로 유지해야 하며, 관계형 데이터의 영구 SoT로 취급하지 않습니다.
|
||||
- Tenant tree, 직급, 직무, 직책은 Ory Keto 관계와 Backend read model을 조합해 제공합니다. Kratos traits는 인증 식별 정보 중심으로 유지해야 하며, Backend DB metadata나 token claim output도 관계형 데이터의 영구 SSOT로 취급하지 않습니다.
|
||||
- Token 크기가 커질 수 있으므로 RP가 긴 조직 전체 정보를 필요로 하면 ID token claim보다 userinfo/profile API 또는 Baron backend API 연동을 우선 검토합니다.
|
||||
- RP는 `lead_tenants` 또는 `tenants.*.lead`만으로 보안상 중요한 권한을 단독 판정하지 않습니다. 권한 변경/민감 리소스 접근은 Keto 기반 Baron authorization contract를 함께 사용해야 합니다.
|
||||
|
||||
|
||||
@@ -11,14 +11,28 @@
|
||||
- **`COMPANY_GROUP`**: B2B2B 지주사/그룹사. 여러 `COMPANY`를 하위로 거느리며 권한을 통합합니다.
|
||||
- **`USER_GROUP`**: 사내 조직 (본부/팀 등). 과거에는 분리된 개념이었으나, 현재는 완벽한 통합을 위해 테넌트의 한 종류로 1:1 매핑됩니다.
|
||||
|
||||
## 2. 외부 백엔드 데이터베이스 의무 채택 (Separation of SoT)
|
||||
## 2. Ory SSOT와 Backend read model 분리
|
||||
|
||||
Kratos 내부 트레이트(Traits)에 테넌트, 직급 등 관계형 데이터를 저장하는 것은 토큰 비대화 및 쿼리 성능 저하를 일으키는 안티 패턴입니다. 따라서 데이터의 진실 공급원(SoT)을 철저히 분리합니다.
|
||||
Kratos 내부 트레이트(Traits)에 테넌트, 직급 등 관계형 데이터를 저장하는 것은 토큰 비대화 및 쿼리 성능 저하를 일으키는 안티 패턴입니다. 하지만 Backend DB를 별도 원장으로 세우지도 않습니다. 권한/관계 판단은 Ory Keto, identity 판단은 Ory Kratos를 기준으로 하고, Backend DB는 Ory에 저장되지 않거나 Ory API로 필요한 조회가 불가능한 조직 표시/검색 데이터의 read model만 보관합니다.
|
||||
|
||||
- **Ory Kratos (Identity SoT)**: 이메일, 패스워드 등 순수 식별 정보만 저장합니다.
|
||||
- **PostgreSQL (Business SoT)**: 반드시 커스텀 외부 백엔드 DB를 구축하여, 테넌트의 트리 구조, 사용자 직급, 애플리케이션 설정 등을 전담하여 관리합니다.
|
||||
- **Ory Kratos (Identity SSOT)**: 이메일, 패스워드 등 인증 식별 정보를 저장합니다.
|
||||
- **Ory Keto (Relationship SSOT)**: 테넌트 소속, 소유, 접근 같은 권한 관계를 저장하고 판정합니다.
|
||||
- **Backend DB read model**: Ory에 저장되지 않거나 조회가 불가능한 테넌트 표시/검색 metadata, 설정, 외부 연동 상태만 저장합니다.
|
||||
|
||||
## 3. 데이터베이스 스키마 분리 전략
|
||||
## 3. Seed tenant 식별 정책
|
||||
|
||||
`adminfront/seed-tenant.csv`에 정의된 초기 테넌트는 CSV의 `id`/`tenant_id` UUID를 source of truth로 삼습니다. `slug`는 운영자가 읽고 외부 연동에서 다루기 쉬운 식별자지만, 오타 수정이나 명칭 정책 변경으로 바뀔 수 있으므로 초기 테넌트 보호 여부와 seed 동기화의 최종 식별 기준으로 사용하지 않습니다.
|
||||
|
||||
- 초기 테넌트 여부는 seed CSV의 UUID와 `tenants.id` 일치 여부로 판단합니다.
|
||||
- `slug`, `name`, `memo`, 도메인 같은 표시/설정 값은 UUID로 식별된 seed tenant의 동기화 대상 metadata입니다.
|
||||
- 기존 DB row가 seed UUID와 일치하지만 `slug`가 CSV와 다르면, backend bootstrap seed 경로는 CSV의 `slug`로 보정해야 합니다.
|
||||
- 목표 `slug`를 다른 활성 tenant가 이미 사용 중이면 자동 보정하지 않고 충돌로 처리합니다.
|
||||
- AdminFront의 “초기 설정” 표시와 삭제 보호도 `slug`가 아니라 seed UUID 기준으로 동작해야 합니다.
|
||||
- 일반 import/export 정책에서는 운영 편의를 위해 `slug`를 우선 사용할 수 있지만, seed tenant의 identity 보존 및 복구 정책은 UUID 기준을 우선합니다.
|
||||
|
||||
예를 들어 한라산업개발 seed row의 UUID가 `5a03efd2-e62f-4243-800d-58334bf48b2f`이면, 기존 DB의 `slug`가 `hanlla`여도 같은 UUID row는 동일 seed tenant로 보고 CSV 값인 `halla`로 보정합니다.
|
||||
|
||||
## 4. 데이터베이스 스키마 분리 전략
|
||||
|
||||
테넌트 테이블의 비대화를 막기 위해, Identity(신분증) 역할과 무거운 Business 데이터를 분리 조인(Join)합니다.
|
||||
|
||||
@@ -26,7 +40,7 @@ Kratos 내부 트레이트(Traits)에 테넌트, 직급 등 관계형 데이터
|
||||
- **`company_settings` 테이블**: `COMPANY` 및 `COMPANY_GROUP` 타입 전용 무거운 비즈니스 설정 (결제 정보, 커스텀 도메인 등).
|
||||
- **`user_groups` 테이블**: `USER_GROUP` 타입 전용 사내 조직도 메타데이터 (`parent_id`, 조직장 정보 등).
|
||||
|
||||
## 4. 논리적 다중 테넌트 OIDC 관리 (Logical Pooling)
|
||||
## 5. 논리적 다중 테넌트 OIDC 관리 (Logical Pooling)
|
||||
|
||||
인프라 비용의 팽창을 막기 위해 테넌트별로 Hydra(OAuth2) 데이터베이스를 물리적으로 복제하는 방식은 금지합니다.
|
||||
대신 공유되는 소수의 Hydra 클러스터 앞단에 도메인 및 헤더를 재작성하는 지능형 프록시를 배치하고, 백엔드의 동의(Consent) 로직을 통해 요청된 클라이언트의 테넌트 맥락에 맞는 **동적 클레임(Dynamic Claim)**을 ID Token에 주입합니다.
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
- 시스템의 모든 자원(예: RelyingParty, 앱)은 반드시 특정 `Tenant`가 소유(`manage`)합니다.
|
||||
- 그러나 자원의 소유권과 **누가 접근할 수 있는가(가시성, `access`)는 별개**입니다. 내부망용 앱(Private)과 대국민 서비스(Public)를 동일한 기업(Tenant)이 동시에 소유하고 제어할 수 있습니다.
|
||||
|
||||
### 1.4 외부 백엔드 데이터베이스 아키텍처 의무 채택 (Separation of SoT)
|
||||
### 1.4 Ory SSOT와 Backend read model 분리
|
||||
사용자 데이터를 Kratos의 내부 트레이트(Traits)에 무분별하게 저장하는 것은 안티 패턴입니다. 이는 토큰 비대화와 쿼리 성능 저하를 초래합니다.
|
||||
- **Kratos (Identity):** "누구인가?" (인증, 이메일, 패스워드 등 순수 식별 정보). 테넌트, 직급 등 관계형 데이터는 절대 보관하지 않습니다.
|
||||
- **PostgreSQL (Business):** "어디에 속하며 조직 구조는 어떠한가?" (직급, 조직도, 테넌트 설정 등).
|
||||
- **Backend DB read model:** Ory에 저장되지 않거나 Ory API로 필요한 조회가 불가능한 조직 표시/검색 metadata, 테넌트 설정, 외부 연동 상태만 보관합니다.
|
||||
- **Keto (ReBAC Authorization Backbone):** "무엇을 할 수 있는가?" (권한 및 상속).
|
||||
|
||||
---
|
||||
@@ -140,9 +140,9 @@ graph TD
|
||||
- 모든 테넌트(`COMPANY`, `PERSONAL`)는 소수의 공유된 Hydra 클러스터를 사용합니다.
|
||||
- Hydra 클러스터 앞단에 도메인 및 헤더를 재작성하는 지능형 프록시(API Gateway)를 배치하여, 테넌트별로 물리적으로 분리된 것과 같은 라우팅 효과를 제공합니다.
|
||||
|
||||
### 5.2 동적 클레임 주입 (Dynamic Claim Injection)
|
||||
- 로그인 및 동의(Consent) 흐름은 전적으로 외부 백엔드 데이터베이스(Business SoT)가 주도합니다.
|
||||
- 백엔드는 요청된 클라이언트(RP)의 테넌트 맥락(Context)을 파악하고, 유저가 속한 현재 조직 정보 및 권한(Role)을 Hydra에 전달하여 **ID Token의 Custom Claim으로 동적 주입**합니다.
|
||||
### 5.2 동적 클레임 조립 (Dynamic Claim Assembly)
|
||||
- 로그인 및 동의(Consent) 흐름의 프로토콜 원장은 Ory Hydra입니다.
|
||||
- 백엔드는 Ory에서 확인한 identity/relationship과 허용된 read model을 조합해 요청된 클라이언트(RP)의 테넌트 맥락(Context)을 계산하고, Hydra에 전달할 claim을 조립합니다.
|
||||
|
||||
---
|
||||
|
||||
@@ -160,4 +160,4 @@ Kratos 웹훅 통신 지연이나 이중 쓰기(Dual-Write) 오류로 인한 '
|
||||
|
||||
### 6.3 삭제 정책 (Cascade) 및 정기 대사
|
||||
- **즉시 회수:** 백엔드 DB에서 Soft Delete(`deleted_at`)가 발생하면, Outbox 워커는 지연 없이 즉각적으로 Keto의 튜플을 Hard Delete 합니다.
|
||||
- **정기 대사 (Reconciliation):** Kratos(Identity), PostgreSQL(DB), Keto(ReBAC) 3자 간의 불일치(고아 튜플, 누락된 멤버십 등)를 매일 1회 이상 배치 크론 잡을 통해 능동적으로 색출하고 자동 복구/삭제합니다.
|
||||
- **정기 대사 (Reconciliation):** Kratos(Identity), PostgreSQL(DB), Keto(ReBAC) 3자 간의 불일치(고아 튜플, 누락된 멤버십 등)를 매일 1회 이상 배치 크론 잡을 통해 능동적으로 색출하고 자동 복구/삭제합니다.
|
||||
|
||||
190
docs/traefik-production-rp-bootstrap-design.md
Normal file
190
docs/traefik-production-rp-bootstrap-design.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Traefik Production RP Bootstrap Design
|
||||
|
||||
## Context
|
||||
|
||||
프로덕션 배포환경에서는 Baron SSO 앞단에 Traefik이 reverse proxy로 먼저 떠 있고, Traefik dashboard와 보호 대상 라우트도 Baron SSO 인증을 사용해야 한다.
|
||||
|
||||
현재 `config/traefik-compose.yml`은 Traefik과 `traefik-forward-auth`를 사전 구동하는 형태지만 다음 보완이 필요하다.
|
||||
|
||||
- `CLIENT_ID`, `CLIENT_SECRET`, `SECRET`가 파일에 하드코딩되어 있다.
|
||||
- OIDC endpoint가 Baron/Ory Hydra가 아니라 Keycloak style path를 가리킨다.
|
||||
- `traefik-public` external network가 선언만 되어 있고 서비스에 연결되어 있지 않다.
|
||||
- production boot flow에서 Traefik forward-auth RP가 Hydra에 자동 등록되지 않는다.
|
||||
|
||||
관련 이슈: #1221
|
||||
|
||||
## Policy Alignment
|
||||
|
||||
- OAuth2/OIDC client SoT는 Ory Hydra다.
|
||||
- client secret 원문은 Git에 커밋하지 않고 `.env`, 배포 host의 secret file, Gitea Actions secret, 또는 운영 secret store에서 주입한다.
|
||||
- Kratos는 identity SoT, Keto는 authorization relation SoT로 유지한다.
|
||||
- Traefik과 forward-auth가 신뢰할 수 있는 last-hop proxy가 되며, 애플리케이션은 임의 외부 요청의 identity header를 신뢰하지 않는다.
|
||||
- Wiki가 사용 중이므로 실제 Wiki 업데이트 전 검토본은 `docs/` 문서로 둔다.
|
||||
|
||||
## Target Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User[Browser] --> Traefik[Traefik edge proxy]
|
||||
Traefik -->|ForwardAuth| TFA[traefik-forward-auth]
|
||||
TFA -->|OIDC authorize/token/userinfo| Hydra[Ory Hydra public endpoint]
|
||||
Hydra --> UserFront[Baron UserFront login/consent]
|
||||
UserFront --> Backend[Baron Backend]
|
||||
Backend --> Kratos[Ory Kratos]
|
||||
Backend --> Keto[Ory Keto]
|
||||
Traefik --> Dashboard[api@internal dashboard]
|
||||
Traefik --> BaronRoute[Baron app routes]
|
||||
```
|
||||
|
||||
`traefik-forward-auth`는 Hydra confidential client로 등록한다. SPA용 `adminfront`, `devfront`, `orgfront`와 달리 server-side middleware이므로 `client_secret` 기반 client로 다룬다.
|
||||
|
||||
## Configuration Contract
|
||||
|
||||
운영 환경 변수는 다음 이름을 기준으로 둔다.
|
||||
|
||||
| Key | Required | Example | Note |
|
||||
|---|---:|---|---|
|
||||
| `TRAEFIK_DASHBOARD_HOST` | yes | `traefik.brsw.kr` | Traefik dashboard host |
|
||||
| `TRAEFIK_FORWARD_AUTH_HOST` | yes | `auth.brsw.kr` | forward-auth callback host |
|
||||
| `TRAEFIK_FORWARD_AUTH_CLIENT_ID` | yes | `traefik-forward-auth` | Hydra client id |
|
||||
| `TRAEFIK_FORWARD_AUTH_CLIENT_SECRET` | yes in production | secret value | Git에 저장하지 않는다 |
|
||||
| `TRAEFIK_FORWARD_AUTH_COOKIE_SECRET` | yes in production | 32+ byte random | forward-auth cookie signing secret |
|
||||
| `TRAEFIK_FORWARD_AUTH_CALLBACK_URLS` | yes | `https://auth.brsw.kr/_oauth` | comma-separated |
|
||||
| `HYDRA_PUBLIC_URL` | yes | `https://app.brsw.kr/oidc` | Baron/Ory public issuer base |
|
||||
|
||||
`config/traefik-compose.yml`은 위 값을 직접 박지 않고 `${...}` placeholder만 사용한다. 여기서 금지하는 것은 "운영 secret 원문을 코드/문서/compose 파일에 커밋하는 것"이다. 배포 이후에는 발급된 client id/secret/cookie secret을 운영 설정 또는 저장소 secret에 고정해 재사용해야 한다.
|
||||
|
||||
## Compose Design
|
||||
|
||||
`config/traefik-compose.yml`의 방향은 다음과 같다.
|
||||
|
||||
- `traefik`와 `forward-auth` 모두 `traefik-public` external network에 붙인다.
|
||||
- `forward-auth` service에는 `traefik.http.services.forward-auth.loadbalancer.server.port=4181` label을 명시한다.
|
||||
- dashboard router에는 `auth-forward@docker` middleware를 적용한다.
|
||||
- forward-auth provider endpoint는 `HYDRA_PUBLIC_URL` 기반 `generic-oauth` 설정으로 산출한다. Traefik이 Baron/Ory보다 먼저 떠야 하는 bootstrap 순서에서는 OIDC discovery가 시작 시점에 실패할 수 있으므로 명시 endpoint 방식을 사용한다.
|
||||
- auth: `${HYDRA_PUBLIC_URL}/oauth2/auth`
|
||||
- token: `${HYDRA_PUBLIC_URL}/oauth2/token`
|
||||
- userinfo: `${HYDRA_PUBLIC_URL}/userinfo`
|
||||
- `INSECURE_COOKIE=false`를 production 기본값으로 둔다.
|
||||
- `CLIENT_SECRET`과 cookie `SECRET`는 운영 secret 주입만 허용한다.
|
||||
|
||||
Baron app compose 쪽에서도 production-facing 진입 서비스는 `traefik-public`에 붙어야 한다. 기존 정책상 외부 진입은 gateway/Oathkeeper 계층으로 수렴하는 것이 맞으므로, Traefik이 직접 backend admin endpoint로 붙는 구조는 피한다.
|
||||
|
||||
## Hydra Client Bootstrap Design
|
||||
|
||||
현재 `compose.ory.yaml`의 `init-rp`는 Hydra 준비 후 기본 RP를 등록한다. Traefik forward-auth RP도 같은 부팅 경로에 넣되, production mode에서만 활성화한다.
|
||||
|
||||
권장 조건:
|
||||
|
||||
```sh
|
||||
APP_ENV in production|prod
|
||||
TRAEFIK_FORWARD_AUTH_ENABLED=true
|
||||
```
|
||||
|
||||
등록 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "traefik-forward-auth",
|
||||
"client_name": "Traefik Forward Auth",
|
||||
"client_secret": "<from env>",
|
||||
"grant_types": ["authorization_code", "refresh_token"],
|
||||
"response_types": ["code"],
|
||||
"scope": "openid offline_access profile email",
|
||||
"redirect_uris": ["https://auth.brsw.kr/_oauth"],
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"metadata": {
|
||||
"managed_by": "baron-sso-boot",
|
||||
"system_client": true,
|
||||
"purpose": "traefik-forward-auth",
|
||||
"status": "active"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
구현은 delete/create보다 idempotent upsert가 안전하다. 최초 부팅 시에는 값이 없으면 생성값을 발급할 수 있지만, RP가 한번 등록된 뒤에는 같은 `client_id`, `client_secret`, callback URI를 운영 설정으로 고정해야 한다. 재배포마다 secret이 바뀌면 forward-auth cookie/session과 Hydra client 인증이 깨질 수 있으므로 rotation은 별도 운영 작업으로만 수행한다.
|
||||
|
||||
1. `hydra get oauth2-client <client_id>`로 존재 여부 확인
|
||||
2. 없으면 env/secret store 값을 사용해 `hydra create oauth2-client`
|
||||
3. 없고 초기 생성 모드가 명시되어 있으면 random secret을 생성해 운영자가 저장할 수 있게 출력 또는 secret file에 기록
|
||||
4. 있으면 `hydra update oauth2-client`로 redirect URI, scope, metadata 같은 비밀값이 아닌 설정을 동기화
|
||||
5. client secret은 기본적으로 덮어쓰지 않고, rotation flag가 있을 때만 갱신
|
||||
6. secret mismatch나 필수 redirect URI 누락 시 실패
|
||||
|
||||
기본 RP 등록 shell block이 길어지고 있으므로, 구현 단계에서는 `scripts/register_hydra_clients.sh` 같은 별도 boot script로 분리하는 편이 유지보수에 유리하다.
|
||||
|
||||
## Generated Auth Config
|
||||
|
||||
`scripts/auth_config.sh`는 다음을 확장한다.
|
||||
|
||||
- `TRAEFIK_FORWARD_AUTH_CALLBACK_URLS` parsing
|
||||
- production mode에서 Traefik client id/secret/cookie secret 필수 검증
|
||||
- generated `config/.generated/auth-config.env`에 Traefik callback CSV 포함
|
||||
- `KRATOS_ALLOWED_RETURN_URLS_JSON`에 forward-auth callback URL 포함
|
||||
- `verify` mode에서 runtime Hydra client에 Traefik callback이 들어 있는지 확인
|
||||
|
||||
운영 secret 보관 위치는 두 가지를 허용한다.
|
||||
|
||||
- 배포 host 기준: `.env` 또는 Docker secret/secret file로 주입
|
||||
- 저장소 기준: Gitea Actions secret에 저장하고 배포 workflow에서 `.env` 또는 secret file로 렌더링
|
||||
|
||||
두 경우 모두 Git tracked 파일에는 실제 secret 값을 남기지 않는다.
|
||||
|
||||
이렇게 하면 Ory 렌더링과 Hydra RP 등록이 같은 auth config contract를 공유한다.
|
||||
|
||||
## Test Plan
|
||||
|
||||
구현 전에 RED test를 먼저 추가한다.
|
||||
|
||||
1. `test/traefik_forward_auth_config_policy_test.sh`
|
||||
- `config/traefik-compose.yml`에 literal `CLIENT_SECRET=` 또는 `SECRET=` 값이 있으면 실패
|
||||
- `traefik`와 `forward-auth`가 `traefik-public` network에 붙지 않으면 실패
|
||||
- `PROVIDER_GENERIC_*_URL`이 Keycloak path를 사용하면 실패
|
||||
- dashboard router에 `auth-forward` middleware가 없으면 실패
|
||||
|
||||
2. `test/auth_config_traefik_rp_policy_test.sh`
|
||||
- `APP_ENV=production`에서 Traefik client secret이 없으면 `scripts/auth_config.sh validate` 실패
|
||||
- callback URL이 `/_oauth` 형태가 아니거나 http URL이면 실패
|
||||
- generated env에 `TRAEFIK_FORWARD_AUTH_CALLBACK_URLS`가 없으면 실패
|
||||
|
||||
3. `test/compose_ory_traefik_rp_bootstrap_policy_test.sh`
|
||||
- `init-rp` 또는 분리된 boot script가 Traefik RP를 등록하지 않으면 실패
|
||||
- boot flow가 delete/create only 방식이면 실패하고 upsert 방식을 요구
|
||||
|
||||
4. live verification
|
||||
- `hydra get oauth2-client --endpoint "$HYDRA_ADMIN_URL" "$TRAEFIK_FORWARD_AUTH_CLIENT_ID"`
|
||||
- dashboard 접근 시 Hydra authorize redirect 발생
|
||||
- callback URL이 registered redirect URI와 정확히 일치
|
||||
- `docker network inspect traefik-public`에서 `traefik`, `forward-auth`, production entry service 연결 확인
|
||||
|
||||
## Rollout Sequence
|
||||
|
||||
1. `.env.sample`에 Traefik forward-auth 변수를 placeholder로 추가한다.
|
||||
2. RED policy tests를 추가하고 실패를 확인한다.
|
||||
3. `config/traefik-compose.yml`을 env-driven Baron/Ory 설정으로 교체한다.
|
||||
4. `scripts/auth_config.sh`와 Hydra RP bootstrap script를 확장한다.
|
||||
5. `compose.ory.yaml`의 `init-rp`가 production에서 Traefik RP upsert를 실행하도록 연결한다.
|
||||
6. Baron SSO production deployment template의 public-facing services에 Traefik labels와 `traefik-public` external network를 연결한다.
|
||||
7. `make validate-auth-config`, 관련 shell policy tests, compose config 검증을 통과시킨다.
|
||||
8. 운영 환경에서 live verification을 수행한다.
|
||||
|
||||
## Baron SSO Deployment Labels
|
||||
|
||||
`deploy/templates/docker-compose.yaml`에서 외부 진입 서비스는 다음 Traefik router를 갖는다.
|
||||
|
||||
| Service | Host variable | Internal port | Purpose |
|
||||
|---|---|---:|---|
|
||||
| `gateway` | `PUBLIC_HOST` | 80 | SSO main app, `/api`, `/auth`, `/oidc` |
|
||||
| `adminfront` | `ADMINFRONT_HOST` | 5173 | Admin console |
|
||||
| `devfront` | `DEVFRONT_HOST` | 5173 | Developer/RP console |
|
||||
| `orgfront` | `ORGFRONT_HOST` | 5175 | Organization console |
|
||||
|
||||
각 public-facing service는 내부 `app_net`과 external `traefik_public`에 동시에 연결한다. `traefik_public`의 실제 Docker network name은 `.env`의 `TRAEFIK_PUBLIC_NETWORK`로 관리하며 기본값은 `traefik-public`이다.
|
||||
|
||||
`deploy/create-instance.sh`는 `TARGET_DIR=/home/user/prod.baron-sso`처럼 레포 밖 고정 경로에 생성할 수 있고, 이 경우에도 compose build context가 깨지지 않도록 `.env`의 `SOURCE_ROOT`를 실행 중인 레포 루트 절대 경로로 채운다.
|
||||
|
||||
## Open Decisions
|
||||
|
||||
- `TRAEFIK_FORWARD_AUTH_HOST`를 `auth.brsw.kr`로 분리할지, `app.brsw.kr/_oauth` 같은 동일 host callback으로 둘지 결정이 필요하다.
|
||||
- `External RP Ory IAM Foundation` 마일스톤은 현재 Due Date가 비어 있다. 구현 착수 전에 목표 Due Date를 정하는 것이 좋다.
|
||||
- `traefik-forward-auth`를 계속 사용할지, Baron Backend/Oathkeeper에 first-party forward-auth endpoint를 만들지는 별도 장기 개선안으로 남긴다. 단기 목표는 현재 compose 구조를 안전하게 Baron/Ory에 맞추는 것이다.
|
||||
@@ -1,14 +1,14 @@
|
||||
# 사용자 projection 가시성 감사 보고서
|
||||
# 사용자 가시성 감사 보고서
|
||||
|
||||
작성 시각: 2026-06-08 16:55 KST
|
||||
|
||||
관련 이슈:
|
||||
- #1035: adminfront 사용자 레지스트리 total이 Kratos 250건 제한으로 잘못 표시됨
|
||||
- #1036: 사용자 projection 가시성 영향 범위 검증 및 WORKS 비교 표 row count 표시
|
||||
- #1036: 사용자 가시성 영향 범위 검증 및 WORKS 비교 표 row count 표시
|
||||
|
||||
## 결론
|
||||
|
||||
`총 250명` 표시는 단순 UI 문제가 아니라, Kratos partial list를 full snapshot처럼 처리한 projection 동기화 버그였습니다.
|
||||
`총 250명` 표시는 단순 UI 문제가 아니라, Kratos partial list를 full snapshot처럼 처리한 legacy sync 버그였습니다.
|
||||
|
||||
현재 로컬 DB와 API는 다음 상태입니다.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
| --- | ---: | --- |
|
||||
| users 전체 | 2,114 | `deleted_at` 포함 전체 row |
|
||||
| visible users | 1,917 | `deleted_at IS NULL`, adminfront/orgfront 사용자 목록 기준 |
|
||||
| soft-deleted users | 197 | 사용자 삭제 또는 과거 projection 문제로 숨겨진 row |
|
||||
| soft-deleted users | 197 | 사용자 삭제 또는 과거 legacy sync 문제로 숨겨진 row |
|
||||
| CSV 원본 줄 수 | 1,887 | 헤더 포함 |
|
||||
| CSV 실제 데이터 행 | 1,886 | 헤더 제외 |
|
||||
| 이번 import 사용자 | 1,886 | 모두 DB 매칭, 모두 visible |
|
||||
@@ -33,8 +33,8 @@
|
||||
|
||||
보고 파일 위치:
|
||||
|
||||
- `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`
|
||||
- `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`
|
||||
|
||||
파일 내용:
|
||||
|
||||
@@ -100,9 +100,9 @@ soft-deleted 기존 사용자 197명과 WORKS comparison `baronId`를 대조한
|
||||
|
||||
이번 문제는 단일 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을 다른 방식으로 소비하고 있었습니다.
|
||||
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가 필요했습니다.
|
||||
|
||||
@@ -110,14 +110,14 @@ soft-deleted 기존 사용자 197명과 WORKS comparison `baronId`를 대조한
|
||||
|
||||
현재 구조는 다음과 같습니다.
|
||||
|
||||
- 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 조회입니다.
|
||||
- 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는 Kratos가 아니라 local projection DB를 primary source로 사용합니다.
|
||||
- Kratos partial list에 없는 사용자를 projection sync에서 삭제하지 않도록 수정했습니다.
|
||||
- 사용자 목록 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를 표시했습니다.
|
||||
|
||||
134
docs/wiki-ory-ssot-cache-policy-update-2026-06-10.md
Normal file
134
docs/wiki-ory-ssot-cache-policy-update-2026-06-10.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Wiki Ory SSOT 및 Redis Cache 정책 업데이트 초안
|
||||
|
||||
작성일: 2026-06-10
|
||||
|
||||
## 목적
|
||||
|
||||
Wiki에 남아 있는 Backend DB 원장화 기준과 claim output 원장화 기준을 폐기하고 다음 정책으로 통일합니다.
|
||||
|
||||
- 인증 identity 원장: Ory Kratos
|
||||
- 권한/관계 원장: Ory Keto
|
||||
- OAuth/OIDC 원장: Ory Hydra
|
||||
- Backend DB: Ory에 저장되지 않거나 Ory API로 필요한 방식의 조회가 불가능한 데이터의 read model
|
||||
- Redis: Ory 또는 허용된 read model의 성능 cache/mirror
|
||||
- Front/API 전송: Ory에서 Redis cache로 웜업된 데이터를 Backend가 cursor 기반 API로 제공
|
||||
|
||||
## Wiki 검색 결과
|
||||
|
||||
Gitea Wiki를 조회한 결과, 다음 페이지는 현재 정책과 충돌하는 문구가 있어 업데이트가 필요합니다.
|
||||
|
||||
| Wiki page | 확인된 문제 | 권장 조치 |
|
||||
| --- | --- | --- |
|
||||
| `Data SoT Architecture Policy` | Backend DB 중심 admin list read path와 async write-behind를 기본 write path로 설명합니다. | 아래 대체 본문으로 교체합니다. |
|
||||
| `[Architecture] Kratos SoT Consolidation` | 관리 read-model의 원장을 Backend DB로 설명하고 Kratos 데이터를 DB에 복제한다고 설명합니다. | 아래 대체 본문으로 교체합니다. |
|
||||
| `tenant-policy.-` | Wiki page 조회 이름이 불안정해 직접 본문 확인은 실패했습니다. 로컬 `docs/tenant-policy.md`와 같은 내용이면 Backend DB 원장화 문구를 삭제해야 합니다. | `docs/tenant-policy.md` 변경본 기준으로 동기화합니다. |
|
||||
| `유저-그룹-및-테넌트-통합-권한-정책` | Wiki page 조회 이름이 불안정해 직접 본문 확인은 실패했습니다. 로컬 `docs/tenant-usergroup-policy.md`와 같은 내용이면 Backend DB 주도 consent 문구를 삭제해야 합니다. | `docs/tenant-usergroup-policy.md` 변경본 기준으로 동기화합니다. |
|
||||
|
||||
## `Data SoT Architecture Policy` 대체 본문
|
||||
|
||||
```md
|
||||
# Baron SSO Data SoT Architecture Policy
|
||||
|
||||
## 1. Core Principle: Ory Stack is the Single Source of Truth
|
||||
|
||||
Baron SSO에서 인증 identity, 권한 관계, OAuth/OIDC 위임의 원장은 Ory Stack입니다.
|
||||
|
||||
- Identity/profile 인증 원장: Ory Kratos
|
||||
- Authorization/ReBAC 원장: Ory Keto
|
||||
- OAuth/OIDC client, consent, token state 원장: Ory Hydra
|
||||
|
||||
Backend DB는 Ory를 대체하는 원장이 아닙니다. Ory에 저장되지 않거나 Ory API로 필요한 방식의 조회가 불가능한 업무 데이터의 read model, 감사 로그, 처리 상태, 성능 cache 보조 데이터만 허용합니다.
|
||||
|
||||
Ory에서 Redis cache로 웜업된 데이터는 Backend가 cursor 기반 API로 front 또는 외부 API에 제공합니다. frontend는 Redis나 Backend DB 복제본을 원장처럼 직접 소비하지 않습니다.
|
||||
|
||||
## 2. Component Policies
|
||||
|
||||
### 2.1 Identity & User Profile
|
||||
|
||||
- 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입니다.
|
||||
|
||||
### 2.2 Permissions & Relationships
|
||||
|
||||
- 권한 판단과 관계 tuple의 원장은 Ory Keto입니다.
|
||||
- Backend DB는 relation command outbox, 처리 상태, 조직 표시/검색에 필요한 read model을 보관할 수 있습니다.
|
||||
- 보안상 중요한 권한 판정은 Backend DB metadata나 token claim만으로 수행하지 않고 Keto check를 거쳐야 합니다.
|
||||
|
||||
### 2.3 OAuth2 Clients & Sessions
|
||||
|
||||
- OAuth2 client, consent, token state의 프로토콜 원장은 Ory Hydra입니다.
|
||||
- `client_consents` 같은 Backend read model은 Hydra가 제공하지 않는 조회 축을 보완하기 위한 모델입니다.
|
||||
- client secret 원문처럼 Hydra가 해시만 보관하는 값은 재발급/운영 목적의 별도 보관 정책과 감사 로그를 가져야 합니다.
|
||||
|
||||
## 3. Data Flow & Synchronization Strategy
|
||||
|
||||
### 3.1 Write Path
|
||||
|
||||
1. 클라이언트 또는 운영 도구가 Backend API/CLI를 호출합니다.
|
||||
2. Backend가 중앙 service를 통해 Ory API를 동기 호출합니다.
|
||||
3. Ory write 성공 후 Ory ID로 재조회합니다.
|
||||
4. Redis mirror를 갱신하거나 갱신 실패 시 `stale`/`failed` 상태를 기록합니다.
|
||||
5. Ory에 저장되지 않거나 조회 불가능한 read model만 Backend DB에 갱신합니다.
|
||||
|
||||
### 3.2 Read Path
|
||||
|
||||
- Self context: Ory session/token 또는 Ory API를 기준으로 검증합니다.
|
||||
- Admin/list context: Backend가 Redis mirror와 허용된 read model을 조합해 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보다 우선하는 근거로 사용하지 않습니다.
|
||||
```
|
||||
|
||||
## `[Architecture] Kratos SoT Consolidation` 대체 본문
|
||||
|
||||
```md
|
||||
# [Architecture] Kratos SoT Consolidation & Redis Cache Strategy
|
||||
|
||||
이 문서는 Kratos identity SSOT와 Redis cache 전략을 정의합니다.
|
||||
|
||||
## 1. Identity Source
|
||||
|
||||
- 원장: Ory Kratos identity
|
||||
- 중앙 write path: Backend `IdentityWriteService`
|
||||
- Redis: identity mirror/cache
|
||||
- Backend DB: Ory에 저장되지 않거나 Ory API로 필요한 조회가 불가능한 Baron 운영 데이터의 read model
|
||||
|
||||
`role`, `tenant_id`, 조직 표시 metadata를 Kratos traits에 무제한 추가하거나 Backend DB를 별도 identity 원장으로 삼지 않습니다.
|
||||
|
||||
## 2. Redis Cache Strategy
|
||||
|
||||
Redis는 성능 cache입니다. Ory에서 Redis cache로 웜업된 데이터는 Backend가 cursor 기반 API로 front 또는 외부 API에 제공합니다.
|
||||
|
||||
- `identity:mirror:{identityID}`: Kratos identity summary 단건 cache
|
||||
- `identity:index:*`: Backend cursor API용 identity 목록/검색 index
|
||||
- `identity:mirror:state`: mirror 상태, count, last error
|
||||
|
||||
Cache miss가 발생한 단건 조회는 Kratos `GetIdentity`로 fallback하고, 성공 시 Redis를 갱신합니다. 목록 조회는 mirror 상태가 `ready`가 아니면 API 응답에 경고 상태를 포함합니다.
|
||||
|
||||
## 3. Write Path Guard
|
||||
|
||||
Kratos identity 변경은 `IdentityWriteService` 경유를 강제합니다.
|
||||
|
||||
- `backend/internal/handler/dev_handler.go`: RP custom claim 관련 잔여 Kratos traits sync도 중앙 service를 경유합니다.
|
||||
- `backend/cmd/adminctl/worksmobile_sync.go`: WORKS 기준 Baron 보정도 중앙 service를 경유합니다.
|
||||
- Kratos Admin API나 Kratos DB 직접 수정은 maintenance guard와 mirror stale 표시 없이 금지합니다.
|
||||
|
||||
## 4. Read Path Guard
|
||||
|
||||
Admin/list 화면과 조직도/picker는 Backend cursor API 또는 Redis orgchart snapshot API를 사용합니다. `limit=5000&offset=0` 같은 단일 대량 offset 조회는 신규 구현에서 금지합니다.
|
||||
|
||||
## 5. Allowed Backend Data
|
||||
|
||||
Backend DB에 허용되는 데이터는 다음 범위입니다.
|
||||
|
||||
- Ory에 저장되지 않는 외부 연동 상태
|
||||
- Ory API로 필요한 조회 축이 제공되지 않는 운영 read model
|
||||
- 감사 로그와 처리 상태
|
||||
- token/userinfo claim assembly에 필요한 RP 범위 metadata
|
||||
|
||||
이 데이터는 Ory SSOT를 대체하지 않습니다.
|
||||
```
|
||||
121
docs/works-drive-docker-image-archive.md
Normal file
121
docs/works-drive-docker-image-archive.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# WORKS Drive Docker Image Archive
|
||||
|
||||
## 목적
|
||||
|
||||
WORKS Drive는 Docker Registry HTTP API v2 backend로 직접 사용하지 않는다. 대신 프로덕션 배포용 Docker 이미지를 `docker save` 결과물로 내보내고, zstd 압축 archive와 검증 파일을 WORKS Shared Drive에 보관하는 CLI 기반 보조 저장소로 사용한다.
|
||||
|
||||
이 방식은 다음 상황을 목표로 한다.
|
||||
|
||||
- Harbor 또는 공용 Registry 장애 시 수동 복구용 이미지 보관
|
||||
- 작은 규모의 프로덕션 배포 이미지 이관
|
||||
- `docker load` 기반 오프라인 배포
|
||||
|
||||
## 저장 구조
|
||||
|
||||
기본 최상위 디렉터리는 다음 환경 변수로 지정한다.
|
||||
|
||||
```dotenv
|
||||
WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR=docker-build-image
|
||||
```
|
||||
|
||||
이미지는 WORKS Shared Drive에서 다음 구조로 저장한다.
|
||||
|
||||
```text
|
||||
docker-build-image/<repository-path>/<tag>/
|
||||
image.tar.zst
|
||||
image.tar.zst.sha256
|
||||
manifest.json
|
||||
```
|
||||
|
||||
예시:
|
||||
|
||||
```text
|
||||
docker-build-image/baron_sso/backend/v1.2606.ab12/
|
||||
image.tar.zst
|
||||
image.tar.zst.sha256
|
||||
manifest.json
|
||||
```
|
||||
|
||||
Registry hostname은 저장 경로에서 제외한다. 예를 들어 `registry.example/baron_sso/backend:v1.2606.ab12`는 `baron_sso/backend/v1.2606.ab12` 아래에 저장한다.
|
||||
|
||||
## Manifest
|
||||
|
||||
`manifest.json`에는 다음 정보를 기록한다.
|
||||
|
||||
- archive format: `docker-save-zstd`
|
||||
- 원본 `image_ref`
|
||||
- repository path
|
||||
- tag
|
||||
- Docker image id
|
||||
- Git commit
|
||||
- archive 파일명, 크기, sha256
|
||||
- WORKS Drive remote path
|
||||
- 복원 명령 예시
|
||||
|
||||
복원은 다음 흐름으로 처리한다.
|
||||
|
||||
```bash
|
||||
sha256sum -c image.tar.zst.sha256
|
||||
zstd -d -c image.tar.zst | docker load
|
||||
```
|
||||
|
||||
## 업로드 CLI
|
||||
|
||||
로컬 컨테이너를 먼저 이미지로 commit한 뒤 업로드하려면 다음처럼 실행한다.
|
||||
|
||||
```bash
|
||||
WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR=docker-build-image \
|
||||
WORKS_DOCKER_COMMIT_CONTAINER=baron_backend \
|
||||
DOCKER_IMAGE_REF=registry.example/baron_sso/backend:v1.2606.ab12 \
|
||||
scripts/docker-image/upload_works_drive.sh
|
||||
```
|
||||
|
||||
이미지가 이미 로컬에 있으면 `WORKS_DOCKER_COMMIT_CONTAINER`를 생략한다.
|
||||
|
||||
```bash
|
||||
DOCKER_IMAGE_REF=registry.example/baron_sso/backend:v1.2606.ab12 \
|
||||
scripts/docker-image/upload_works_drive.sh
|
||||
```
|
||||
|
||||
실제 업로드에는 기존 백업 업로드와 같은 WORKS Drive 인증 변수를 사용한다.
|
||||
|
||||
- `WORKS_DRIVE_TARGET=sharedrive`
|
||||
- `WORKS_DRIVE_SHARED_DRIVE_ID` 또는 `WORKS_SHAREDRIVE_ID`
|
||||
- 선택: `WORKS_DRIVE_PARENT_FILE_ID`
|
||||
- `WORKS_DRIVE_ACCESS_TOKEN`, `WORKS_DRIVE_ACCESS_TOKEN_FILE`, `WORKS_DRIVE_ACCESS_TOKEN_CMD`, `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`, 또는 서비스 계정 OAuth 변수
|
||||
|
||||
업로드 전 packaging만 확인하려면 다음을 사용한다.
|
||||
|
||||
```bash
|
||||
WORKS_DRIVE_DRY_RUN=true \
|
||||
DOCKER_IMAGE_REF=registry.example/baron_sso/backend:v1.2606.ab12 \
|
||||
scripts/docker-image/upload_works_drive.sh
|
||||
```
|
||||
|
||||
smoke 검증에는 Alpine 계열보다 운영 환경과 libc/패키지 계열 차이가 적은 Debian slim 계열을 사용한다.
|
||||
|
||||
```bash
|
||||
docker create --name baron-works-image-smoke debian:trixie-slim \
|
||||
sh -c 'printf works-drive-docker-image-smoke >/works-smoke.txt'
|
||||
docker start -a baron-works-image-smoke
|
||||
WORKS_DOCKER_COMMIT_CONTAINER=baron-works-image-smoke \
|
||||
DOCKER_IMAGE_REF=registry.example/baron_sso/works-smoke:works-test-ab12 \
|
||||
scripts/docker-image/upload_works_drive.sh
|
||||
```
|
||||
|
||||
## Staging/Production 계약
|
||||
|
||||
Action에서 `dev` 브랜치를 checkout한 뒤 한 번만 이미지를 빌드하고 immutable `image_tag`를 계산한다. staging과 production은 같은 image_tag를 입력받아 같은 registry image를 pull한다.
|
||||
|
||||
```text
|
||||
dev branch -> publish image tag vX.YYMM.<commit4> -> staging deploy -> production deploy
|
||||
```
|
||||
|
||||
WORKS Drive archive도 Action에서 push된 이미지를 다시 pull한 뒤 `docker save`로 만든다. 따라서 WORKS archive, staging, production은 모두 같은 registry image tag를 기준으로 한다.
|
||||
|
||||
## 제한
|
||||
|
||||
- 이 구조는 `docker push`/`docker pull`과 호환되는 Registry backend가 아니다.
|
||||
- layer deduplication이 없으므로 같은 기반 이미지가 반복 저장된다.
|
||||
- 배포 전에는 반드시 `image.tar.zst.sha256` 검증 후 `docker load`를 수행해야 한다.
|
||||
- tag 없는 image ref와 digest-only image ref는 지원하지 않는다.
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
## 원인
|
||||
|
||||
이전 사용자 projection 동기화 코드가 Kratos `ListIdentities()` 결과를 전체 identity 목록으로 간주했습니다. 해당 API 결과는 제한된 페이지 결과였고, 그 목록에 없던 기존 사용자가 Baron `users`에서 soft-delete 처리되었습니다.
|
||||
이전 사용자 legacy sync 코드가 Kratos `ListIdentities()` 결과를 전체 identity 목록으로 간주했습니다. 해당 API 결과는 제한된 페이지 결과였고, 그 목록에 없던 기존 사용자가 Baron `users`에서 soft-delete 처리되었습니다.
|
||||
|
||||
이로 인해 WORKS에는 사용자가 남아 있고 `externalKey`도 Baron 사용자 UUID를 가리키지만, Baron 비교 로직에서는 soft-deleted 사용자가 visible 사용자로 조회되지 않아 `missing_in_baron`으로 표시되었습니다.
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
테스트:
|
||||
|
||||
- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/service -run 'TestWorksmobileSyncServiceSkipsSoftDeletedUsersInComparison' -count=1`
|
||||
- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/handler ./internal/repository -run 'Test.*User|Test.*Projection|Test.*SoftDeleted|Test.*ListUsers' -count=1`
|
||||
- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/handler ./internal/repository -run 'Test.*User|Test.*SoftDeleted|Test.*ListUsers' -count=1`
|
||||
- `BASE_URL=http://127.0.0.1:5173 npm --prefix adminfront test -- worksmobile.spec.ts --project=chromium`
|
||||
|
||||
결과:
|
||||
@@ -101,8 +101,8 @@
|
||||
|
||||
이미 적용된 코드 변경으로 다음 조건을 방어합니다.
|
||||
|
||||
- admin 사용자 목록은 Kratos 250개 제한 결과가 아니라 로컬 projection repository를 기준으로 조회합니다.
|
||||
- projection replace 동기화는 Kratos partial list에 없는 사용자를 삭제 처리하지 않습니다.
|
||||
- admin 사용자 목록은 Kratos 250개 제한 결과가 아니라 Backend cursor API와 identity mirror 상태를 기준으로 조회합니다.
|
||||
- legacy replace sync는 Kratos partial list에 없는 사용자를 삭제 처리하지 않습니다.
|
||||
- WORKS 비교 로직은 soft-deleted Baron 사용자를 visible 사용자로 취급하지 않습니다.
|
||||
- WORKS 비교 UI에는 필터링 후 표시 row와 전체 row 수를 함께 표시합니다.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## 현재 Baron SSO 구조 요약
|
||||
|
||||
Baron SSO는 Ory Stack을 SoT로 두고, PostgreSQL은 read-model 및 비즈니스 메타데이터 저장소로 사용합니다. `docs/SoT_Architecture_Policy.md`와 `docs/tenant-usergroup-policy.md` 기준으로 Identity는 Kratos, 권한/멤버십은 Keto, 테넌트/조직 메타데이터는 PostgreSQL이 담당합니다.
|
||||
Baron SSO는 Ory Stack을 SSOT로 둡니다. `docs/SoT_Architecture_Policy.md`와 `docs/tenant-usergroup-policy.md` 기준으로 Identity는 Kratos, 권한/멤버십은 Keto가 원장입니다. PostgreSQL은 Ory에 저장되지 않거나 조회가 불가능한 Worksmobile/조직 표시/검색 데이터의 read model과 처리 상태 저장소로만 사용합니다.
|
||||
|
||||
현재 사용자 생성 흐름은 다음과 같습니다.
|
||||
|
||||
@@ -170,15 +170,38 @@ Baron Kratos identity를 Worksmobile user로 보냅니다.
|
||||
- `passwordConfig.password`: 구성원 생성 시 숫자, 영문, 기호를 모두 포함한 16자리 난수 초기 비밀번호를 생성합니다.
|
||||
- `task`: Baron `jobTitle`을 우선 사용
|
||||
- `organizations`
|
||||
- 원직: 대표 tenant 또는 `additionalAppointments` 중 primary로 선택된 tenant
|
||||
- 겸직: `metadata.additionalAppointments` 또는 Keto `joinedTenants`
|
||||
- 원직: Worksmobile 연동 제외 테넌트를 제외한 뒤 `additionalAppointments`에 가장 먼저 등록된 tenant
|
||||
- 겸직: `metadata.additionalAppointments` 또는 Keto `joinedTenants` 중 Worksmobile 연동 제외가 아닌 tenant
|
||||
- `orgUnits[].orgUnitId`: `externalKey:{tenant.ID}`
|
||||
- `levelId`, `positionId`, `userTypeId`: 이번 scope에서는 External Key mapping을 사용하지 않고 사용자 정보 업데이트 필드로 최대한 커버
|
||||
- `isManager`: `additionalAppointments[].isOwner == true` 또는 Keto owners/admins relation을 기준으로 변환
|
||||
|
||||
### 구성원 소속 변경 정책
|
||||
|
||||
Worksmobile 연동 화면의 변경 범위는 다음과 같이 분리합니다.
|
||||
|
||||
- 조직/그룹 탭은 Worksmobile `orgunits`/`groups` 리소스 자체의 생성, 수정, 삭제, 이동을 관리합니다.
|
||||
- 구성원 탭은 Worksmobile `users` 리소스의 조직 소속(`organizations[].orgUnits[]`) 변경을 관리합니다.
|
||||
- 구성원 소속 변경 중 Worksmobile에 대상 org unit이 없거나 부모 구조가 최신이 아니면, 먼저 조직/그룹 탭의 조직 sync로 구조 변경을 반영한 뒤 구성원 sync를 수행합니다.
|
||||
- `worksmobileExcluded=true`인 테넌트와 그 하위 조직은 조직 sync 및 구성원 소속 sync의 대상에서 제외합니다.
|
||||
|
||||
개인 사용자 상세 변경은 대표 조직 1개만 수정하는 동작으로 제한하지 않습니다. Baron의 `metadata.additionalAppointments`에 들어 있는 모든 소속 조직을 기준으로 Worksmobile payload의 `organizations[].orgUnits[]`를 구성합니다. 다만 Worksmobile 연동 제외 테넌트, Worksmobile domain ID를 해석할 수 없는 테넌트, 조직 연동 설정이 켜져 있지 않은 겸직 도메인은 payload에서 제외하고 비교 결과에는 skipped reason 또는 warning을 남깁니다.
|
||||
|
||||
WORKS Developers 문서 확인 결과, 구성원 추가/수정 payload는 `organizations[]`와 하위 `orgUnits[]` 배열을 제공하고 `orgUnits`는 최대 30개까지 허용합니다. 또한 대표 도메인과 대표 조직은 각각 하나가 필요합니다. Directory API의 조직 연동 설명에는 구성원 회사 겸직을 설정하려면 겸직 도메인도 조직 연동 사용 설정을 켜야 한다는 제약이 있습니다.
|
||||
|
||||
따라서 구현 원칙은 다음과 같습니다.
|
||||
|
||||
- 동일 Worksmobile domain 안에서는 Worksmobile 연동 제외가 아닌 조직 소속을 모두 동기화합니다.
|
||||
- 여러 Worksmobile domain을 넘는 겸직은 해당 domain의 조직 연동 설정이 켜져 있고 Baron에서 domain ID를 해석할 수 있을 때만 동기화합니다.
|
||||
- Worksmobile primary는 Baron 대표 테넌트 플래그나 `additionalAppointments[].isPrimary=true`를 기준으로 삼지 않습니다. Worksmobile 연동 제외 테넌트를 제거한 뒤 `additionalAppointments`에 가장 먼저 등록된 소속을 최초 반영 소속이자 primary로 둡니다.
|
||||
- `representative`, `isPrimary`, `primary` 같은 Baron 대표 소속 플래그는 Baron 내부 대표 소속 정책에만 사용하고 Worksmobile 동기화 판단 필드로 사용하지 않습니다.
|
||||
- 조직장 여부(`isManager`)는 Worksmobile 동기화 대상입니다. 비교 로직은 remote와 Baron의 조직장 여부 차이를 `needs_update`로 판단해야 합니다.
|
||||
- 비교 로직도 대표 조직 1개가 아니라 `organizations[].orgUnits[]` 전체 set을 기준으로 `org_unit_added`, `org_unit_removed`, `org_unit_moved`, `org_unit_primary_changed` 같은 diff reason을 산출합니다.
|
||||
- Worksmobile 공식 문서 근거: https://developers.worksmobile.com/kr/docs/user-create, https://developers.worksmobile.com/kr/docs/directory
|
||||
|
||||
초기 비밀번호는 Worksmobile user upsert outbox payload에 `loginEmail`, `initialPassword` 형태로 함께 보관하고, adminfront의 한맥가족 Worksmobile 관리 화면에서 `email,initialPassword,status,lastError` CSV로 다운로드할 수 있게 합니다. 생성 성공/실패 판정은 outbox 작업 상태(`processed`, `failed`)와 함께 확인할 수 있으며, 운영상 평문 초기 비밀번호가 포함되므로 다운로드 권한은 `hanmac-family` tenant manage 권한으로 제한하고 보존 기간 정책을 별도 확정해야 합니다.
|
||||
|
||||
현재 backend `CreateUser`와 `UpdateUser`는 adminfront가 보내는 top-level `additionalAppointments` 및 `metadata.additionalAppointments`를 수용합니다. 한맥가족 단건 생성에서 대표 `tenantSlug` 없이 appointment만 오는 경우에는 first/primary appointment tenant를 대표 tenant로 해석해 Kratos traits, local read-model, Worksmobile enqueue가 누락되지 않게 합니다.
|
||||
현재 backend `CreateUser`와 `UpdateUser`는 adminfront가 보내는 top-level `additionalAppointments` 및 `metadata.additionalAppointments`를 수용합니다. 한맥가족 단건 생성에서 대표 `tenantSlug` 없이 appointment만 오는 경우에는 first/primary appointment tenant를 대표 tenant로 해석해 Ory/Keto 관계, 허용된 Backend read model, Worksmobile enqueue가 누락되지 않게 합니다.
|
||||
|
||||
### 구성원 수정과 비밀번호 정책
|
||||
|
||||
@@ -324,19 +347,19 @@ Worksmobile 운영 화면은 `orgfront`가 아니라 `adminfront`의 tenant deta
|
||||
|
||||
### 신규 사용자 단건 생성
|
||||
|
||||
- 후보 위치: `UserHandler.CreateUser`에서 Kratos 생성, local DB sync, login ID sync, Keto outbox enqueue 후
|
||||
- 후보 위치: `UserHandler.CreateUser`에서 Kratos 생성, 허용된 Backend read model sync, login ID sync, Keto outbox enqueue 후
|
||||
- payload에는 `identityID`, email, name, phone, tenantID, metadata/additionalAppointments를 포함합니다.
|
||||
- `hanmac-family` scope가 아니면 enqueue하지 않습니다.
|
||||
|
||||
### 신규 사용자 bulk 생성
|
||||
|
||||
- 후보 위치: `UserHandler.BulkCreateUsers`에서 row별 local DB sync와 Keto outbox enqueue 후
|
||||
- 후보 위치: `UserHandler.BulkCreateUsers`에서 row별 허용된 Backend read model sync와 Keto outbox enqueue 후
|
||||
- row별 partial success를 유지하고, Worksmobile enqueue 실패는 사용자 생성 실패와 분리하는 것이 좋습니다.
|
||||
- 단, enqueue 실패는 audit/error로 남기고 운영자가 재시도할 수 있어야 합니다.
|
||||
|
||||
### 사용자 수정/소속 변경
|
||||
|
||||
- 후보 위치: `UserHandler.UpdateUser`에서 Kratos update와 local DB sync 후
|
||||
- 후보 위치: `UserHandler.UpdateUser`에서 중앙 `IdentityWriteService` 기반 Kratos update와 허용된 Backend read model sync 후
|
||||
- `email`, `name`, `phone`, `companyCode`, `tenant_id`, `metadata.additionalAppointments` 변경 시 `USER UPSERT` enqueue
|
||||
- `suspended`는 Worksmobile suspend로 동기화합니다.
|
||||
- `temporary_leave`는 Worksmobile 계정을 유지합니다.
|
||||
|
||||
35
docs/worksmobile-phone-outbound-policy-2026-06-15.md
Normal file
35
docs/worksmobile-phone-outbound-policy-2026-06-15.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# WORKS 전화번호 송신 포맷 정책
|
||||
|
||||
작성일: 2026-06-15
|
||||
|
||||
## 목적
|
||||
|
||||
Baron 내부 전화번호 표준과 WORKS API 송신 포맷을 분리한다.
|
||||
|
||||
## 정책
|
||||
|
||||
- Baron 내부 저장 및 비교 표준은 공백 없는 E.164 형태를 유지한다.
|
||||
- 예: `+821041585840`
|
||||
- WORKS 계정 생성 및 업데이트 요청으로 전화번호를 보낼 때는 국가번호와 국내 번호 사이에 공백을 둔다.
|
||||
- 예: `+82 01041585840`
|
||||
- WORKS 송신용 국내 번호는 `0`으로 시작해야 한다.
|
||||
- 내부값 `+821041585840`은 WORKS 송신 시 `+82 01041585840`으로 변환한다.
|
||||
- WORKS 응답 비교는 기존처럼 공백 포함/미포함 형식을 같은 전화번호로 정규화해서 비교한다.
|
||||
|
||||
## 적용 범위
|
||||
|
||||
- WORKS Directory 사용자 생성 요청
|
||||
- WORKS Directory 사용자 업데이트 요청
|
||||
- WORKS SCIM 사용자 생성 요청
|
||||
|
||||
## 비적용 범위
|
||||
|
||||
- Baron DB 저장값
|
||||
- Kratos traits 저장값
|
||||
- Adminfront 비교 화면의 내부 기준 표시값
|
||||
|
||||
## 구현 기준
|
||||
|
||||
- 내부 payload 생성 단계에서는 기존 E.164 정규화 값을 유지한다.
|
||||
- 실제 WORKS API 송신 직전 outbound formatter에서만 `+82 0...` 형식으로 변환한다.
|
||||
- 한국 번호가 아닌 값은 기존 정규화 결과를 유지한다.
|
||||
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