forked from baron/baron-sso
892 lines
51 KiB
Markdown
892 lines
51 KiB
Markdown
# Baron SSO
|
|
|
|
[](https://gitea.hmac.kr/baron/baron-sso/src/branch/dev) [](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) [](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) [](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
|
|
[](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) [](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) [](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) [](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
|
|
[](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/userfront_e2e_full_nightly.yml?branch=dev) [](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/userfront_e2e_full_nightly.yml?branch=dev) [](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/userfront_e2e_full_nightly.yml?branch=dev)
|
|
|
|
badge는 `Code Check`가 `badges` 브랜치의 `latest/`와 `dev/<commit-sha>/`에 발행합니다. 최신 HTML/LCOV/JSON summary는 Gitea `Code Check`의 패키지별 `*-vitest-coverage-report` artifact에서 확인할 수 있습니다.
|
|
|
|
**Baron 로그인**은 화이트 라벨링된 가족사의 모든 소프트웨어 Auth를 총괄하는 사용자 인증/인가 허브입니다.
|
|
|
|
## 📂 프로젝트 구조 (Project Structure)
|
|
|
|
```
|
|
baron_sso/
|
|
├── backend/ # Go Fiber 애플리케이션
|
|
│ ├── cmd/server/ # 진입점 (Entry point)
|
|
│ ├── internal/ # 도메인, 핸들러, 저장소(Repository)
|
|
│ └── Dockerfile
|
|
├── userfront/ # Flutter 애플리케이션
|
|
│ ├── src/ # UI 및 로직
|
|
│ └── pubspec.yaml
|
|
├── adminfront/ # React 기반 관리
|
|
│ ├── src/ # UI 및 로직
|
|
│ └── pubspec.yaml
|
|
├── gateway/ # Nginx 기반 Gateway (UserFront 프록시)
|
|
├── compose.ory-stack.yaml # DB 서비스 (Postgres, ClickHouse)
|
|
├── compose.infra.yaml # DB 서비스 (Postgres, ClickHouse)
|
|
├── docker-compose.yaml # 앱 서비스 (Front, Back)
|
|
├── .env.sample # 환경 설정 템플릿
|
|
└── README.md # 본 파일
|
|
```
|
|
* Ory Stack으로 모든 구성요소를 self-hosting 합니다.
|
|
* Backend는 Go (Fiber)로 구성된 Ory Stack의 유일한 Command 전송 포인트입니다. 모든 Command는 ClickHouse로 강제 전송되며 Audit Log 시스템을 구성합니다.
|
|
* Front는 Backend를 통해서만 연동하며 자체가 Ory Stack의 RP기도 합니다. 크게 3개 계층으로 분리됩니다.
|
|
* UserFront: Flutter로 구현된 커스텀 UI를 통해 매끄러운 사용자 경험을 보장합니다.
|
|
* 로그인 : 비밀번호, SMS, QR 스캔 등의 수단으로 로그인 가능
|
|
* 향후 모바일 앱 지원으로 인증 Push 승인 등 MFA 확장 (예정)
|
|
* 정보변경, 앱 연동 이력 확인 등 개인화 기능
|
|
* 사용 가능한 앱 리스트 (예정)
|
|
* AdminFront: 사용자 관리 등 Admin 기능
|
|
* DevFront: RP 관리 등 개발자 기능
|
|
|
|
## 개발 실행 정책
|
|
|
|
`make dev`는 로컬 개발용 실행 모드이며, React 기반 `adminfront`, `devfront`, `orgfront`는 모두 Vite HMR 모드로 동작해야 합니다. 이 세 서비스는 Docker Compose에서 Dockerfile `dev` target을 사용하고 `/workspace/<app>` bind mount 위에서 `npm run dev -- --host 0.0.0.0`로 실행합니다. `make dev` 경로에서 production `dist`를 `serve_frontend_prod.mjs`로 정적 서빙하면 안 됩니다.
|
|
|
|
현재 개발 포트는 다음과 같습니다.
|
|
|
|
- AdminFront: `http://localhost:5173`
|
|
- DevFront: `http://localhost:5174`
|
|
- OrgFront: `http://localhost:5175`
|
|
|
|
자세한 정책과 회귀 테스트는 [make dev Vite HMR Policy](docs/make-dev-vite-hmr-policy.md)를 확인하세요. 정책 회귀는 `test/frontend_dev_bind_mount_policy_test.sh`에서 검사합니다.
|
|
|
|
로컬 Playwright E2E도 기본적으로 Vite dev server를 봅니다. Gitea Actions 같은 CI에서는 `CI=true`로 production bundle을 `vite preview`로 검증합니다. 로컬에서 production bundle을 명시적으로 검증하려면 `PLAYWRIGHT_USE_PREVIEW=true`를 사용하세요. 이 정책은 `test/playwright_frontend_runtime_policy_test.sh`에서 검사합니다.
|
|
|
|
|
|
## 🏗 아키텍처 (Architecture)
|
|
|
|
### 0. Ory Stack
|
|
- Ory Kratos: 사용자 인증/계정 관리(Identity).
|
|
- Ory Hydra: OAuth2/OIDC 발급 및 토큰 관리.
|
|
- Ory Keto: 권한/정책 기반 접근 제어.
|
|
- Oathkeeper: 인증/인가 프록시 및 라우팅 게이트웨이.
|
|
|
|
```mermaid
|
|
flowchart
|
|
subgraph Edge
|
|
OK["Oathkeeper<br/>(Only Public Entry)"]
|
|
end
|
|
|
|
subgraph App
|
|
BE["Backend<br/>(Only Upstream)"]
|
|
end
|
|
|
|
subgraph OryStack
|
|
KR[Kratos]
|
|
HY[Hydra]
|
|
KE[Keto]
|
|
KR --- HY --- KE
|
|
end
|
|
BE -->|Command| OryStack
|
|
OK -->|Query| KR
|
|
OK -->|Query| HY
|
|
OK -->|Query| KE
|
|
```
|
|
|
|
### 1. Backend (Go Fiber)
|
|
- **Language**: Go 1.26.2+
|
|
- **Framework**: Fiber v2.25+
|
|
- **Database**:
|
|
- **ClickHouse**: 감사 로그 (고성능 데이터 수집)
|
|
- **PostgreSQL**: 메타데이터 저장소 (Primary)
|
|
- **Features**:
|
|
- 인증용 SMS 발송 등 Ory-Stack으로 구현 어려운 부분 직접 구현
|
|
- `POST /api/v1/audit`: 감사 로그 수집 API
|
|
- userfront가 바라보는 backend
|
|
|
|
### 2. UserFront(Flutter Web/App)
|
|
- **Framework**: Flutter 3.38.0+
|
|
- **Key Packages**: `flutter_riverpod`, `go_router`
|
|
- **Features**:
|
|
- 탭 기반 로그인 UI (비밀번호 기반 / 링크 기반 / QR 기반 등)
|
|
|
|
### 3. adminfront(Web)
|
|
- **Framework**: Vite, React 19+, Shadcn/ui 등
|
|
- **Features**:
|
|
- 사용자 관리, 권한 부여 등 관리자 기능
|
|
- 앱 별 사용량(호출량) 등 통계
|
|
- 핵심 Audit 대상
|
|
|
|
### 4. devfront(Web)
|
|
- **Framework**: Vite, React 19+, Shadcn/ui 등
|
|
- **Features**:
|
|
- RP 등록 및 관리
|
|
- RP별 Consent 관리
|
|
|
|
## 관리 데이터 Export/Import 정책
|
|
|
|
AdminFront의 테넌트와 사용자 export/import는 운영자가 CSV를 직접 검토하고 재반입할 수 있는 흐름을 기준으로 설계합니다. 기본 원칙은 내부 UUID를 불필요하게 노출하지 않고, 사람이 이해하기 쉬운 `slug`와 이름을 우선 사용하는 것입니다.
|
|
|
|
### 공통 원칙
|
|
- CSV는 Excel 호환을 위해 UTF-8 BOM을 포함해 내려받습니다.
|
|
- 기본 export는 시스템 내부 ID를 제외합니다.
|
|
- 같은 데이터를 정확히 재동기화해야 하는 운영 작업에서는 `includeIds=true` 옵션으로 내부 ID 컬럼을 포함할 수 있습니다.
|
|
- import는 preview/검토 단계를 거친 뒤 실행하는 것을 기본으로 합니다.
|
|
- 기존 데이터와 충돌 가능성이 있는 row는 자동 적용하지 않고 관리자 선택 또는 확인 상태로 표시합니다.
|
|
- 삭제는 export/import로 암묵 처리하지 않습니다. 삭제가 필요하면 별도 삭제 기능을 사용합니다.
|
|
|
|
### Tenant Export
|
|
- 기본 컬럼은 운영자가 다시 import하기 쉬운 형태를 유지합니다.
|
|
- `includeIds=false`가 기본이며, 이 경우 내부 `tenant_id`는 제외합니다.
|
|
- `includeIds=true`를 사용하면 기존 테넌트 update 또는 staging/production 간 매핑 확인에 필요한 ID를 포함합니다.
|
|
- 주요 의미:
|
|
- `tenant_id`: 내부 UUID. 기본 export에서는 제외됩니다.
|
|
- `name`: 테넌트 표시 이름입니다.
|
|
- `type`: `PERSONAL`, `COMPANY`, `COMPANY_GROUP`, `USER_GROUP` 중 하나입니다.
|
|
- `parent_tenant_id`: 상위 테넌트 내부 ID입니다.
|
|
- `parent_tenant_slug`: 상위 테넌트를 slug로 연결할 때 사용합니다.
|
|
- `slug`: 운영상 사람이 다루는 테넌트 식별자입니다.
|
|
- `memo`: 설명 또는 비고입니다.
|
|
- `email_domain`: 테넌트에 연결된 이메일 도메인입니다. 여러 도메인은 `;`, `,`, 줄바꿈으로 구분할 수 있습니다.
|
|
|
|
### Tenant Import
|
|
- 필수 컬럼은 `name`, `type`, `slug`입니다.
|
|
- 허용되는 header alias:
|
|
- `tenant_id`: `id`, `tenantid`, `tenant_id`
|
|
- `parent_tenant_id`: `parentid`, `parent_id`, `parenttenantid`, `parent_tenant_id`
|
|
- `parent_tenant_slug`: `parenttenantslug`, `parent_tenant_slug`
|
|
- `memo`: `memo`, `description`
|
|
- `email_domain`: `email-domain`, `emaildomain`, `email_domain`, `domain`, `domains`
|
|
- `tenant_id`가 있고 기존 테넌트가 있으면 update 대상으로 봅니다.
|
|
- `tenant_id`가 없으면 `slug` 기준으로 기존 테넌트를 찾고, 없으면 신규 생성 후보로 봅니다.
|
|
- `parent_tenant_slug`가 같은 import 파일 안에 있으면 부모 row를 먼저 처리하도록 정렬합니다.
|
|
- import preview는 이름/slug 유사도 기반 후보를 보여주며, 관리자가 기존 테넌트 사용, 신규 생성, skip 중 선택할 수 있어야 합니다.
|
|
- 외부 시스템에서 가져온 `tenant_id`처럼 현재 DB에 없는 ID는 충돌로 표시하고, 관리자가 새 slug 또는 기존 테넌트 매핑을 결정해야 합니다.
|
|
|
|
### User Export
|
|
- 기본 컬럼은 `Email`, `Name`, `Phone`, `Status`, `tenant_slug`, `Position`, `JobTitle`, `CreatedAt`입니다.
|
|
- `includeIds=true`이면 `user_id`, `tenant_id`를 함께 포함합니다.
|
|
- 사용자 role은 export 기본 컬럼에서 제외합니다. role은 일괄 변경의 실수 위험이 크므로 명시적 관리 화면 또는 별도 정책으로 다룹니다.
|
|
- 사용자 metadata는 `Meta:<key>` 컬럼으로 뒤에 추가됩니다.
|
|
- `includeIds=false`일 때는 `id`, `user_id`, `tenant_id`, `tenantid` 성격의 metadata key를 export에서 제외합니다.
|
|
- tenant admin의 export는 관리 가능한 테넌트 범위로 제한됩니다.
|
|
|
|
### User Import
|
|
- 사용자 CSV의 기본 컬럼은 `email`, `name`, `phone`, `role`, `tenant_slug`, `department`, `position`, `jobTitle`입니다.
|
|
- `email`과 `name`은 CSV parsing 단계의 필수값입니다.
|
|
- backend 생성 단계에서는 `tenantSlug`도 필수입니다.
|
|
- `tenant`, `tenant_slug`, `companyCode` header는 사용자 소속 테넌트 slug로 매핑됩니다.
|
|
- `tenant_id`, `tenant_name`, `tenant_type`, `parent_tenant_id`, `parent_tenant_slug`, `parent_tenant_name`, `tenant_memo`, `email_domain` 컬럼이 있으면 사용자 import 과정에서 필요한 테넌트 생성/매핑 preview에 사용합니다.
|
|
- 위 기본 컬럼에 속하지 않는 컬럼은 사용자 metadata로 들어갑니다.
|
|
- 테넌트에 `userSchema`가 있으면 import 중 metadata required/validation/loginId 규칙을 적용합니다.
|
|
- 테넌트 schema에서 `isLoginId`로 지정된 metadata 값은 custom login ID로 동기화하며, 이메일/전화번호/예약어와 충돌하지 않아야 합니다.
|
|
|
|
### 한맥가족 User Import Email 정책
|
|
- 전체 시스템에서 `users.email`은 unique입니다.
|
|
- `active`, `temporary_leave`, `suspended`, `preboarding`, `baron_guest`, `extended_leave`, `archived` 등 모든 사용자 상태가 unique 검사 대상입니다. 특히 `preboarding`, `baron_guest`, `archived` 사용자는 email/local-part 선점 대상입니다.
|
|
- 한맥가족 테넌트 root(`hanmac-family`)와 그 하위 subtree에서는 이메일 도메인과 무관하게 `@` 앞 local-part도 unique 해야 합니다.
|
|
- 예: `han@hanmaceng.co.kr`가 한맥가족 구성원으로 있으면 `han@samaneng.com`은 한맥가족 구성원으로 생성할 수 없습니다.
|
|
- `email` 값이 `@hanmaceng.co.kr`처럼 도메인만 있으면 import preview에서 이름 기반 local-part를 제안합니다.
|
|
- 이름 기반 local-part 기본 규칙은 `이름 부분 초성 + 성 로마자`입니다.
|
|
- 예: `한치영` -> `치영`의 초성 `c + y` + 성 `han` -> `cyhan`
|
|
- 이미 `cyhan`, `cyhan1`이 있으면 다음 후보인 `cyhan2`를 제안합니다.
|
|
- 외부 로마자화 패키지는 backend 의존성으로 추가하지 않고, 내부 한글 음절 분해와 성씨/초성 매핑을 사용합니다.
|
|
- import preview의 row 상태:
|
|
- `valid`: unique와 이름 기반 권장 규칙을 모두 만족합니다.
|
|
- `suggested`: 도메인만 있거나 suffix 제안이 필요한 row입니다.
|
|
- `needsReview`: 이름 매핑이 애매해 관리자가 직접 확인해야 합니다.
|
|
- `ruleMismatch`: 최종 local-part가 `이름 이니셜 + 성 + 숫자 suffix` 규칙과 다릅니다. 예외 진행은 가능하지만 관리자에게 표시해야 합니다.
|
|
- `blockingError`: local-part 중복, email 형식 오류, 필수값 누락처럼 생성을 차단해야 하는 상태입니다.
|
|
- 단건 사용자 생성은 한맥가족 local-part 중복 시 자동 제안하지 않고 `409 Conflict`로 차단합니다.
|
|
- bulk import는 preview에서 제안/수정된 최종 email을 사용하되, backend가 생성 직전에 다시 unique 규칙을 검증합니다.
|
|
|
|
### User Status 정책
|
|
| 상태 | 표시명 | Baron 사용 | Works 처리 | 일반 조직도 |
|
|
| --- | --- | --- | --- | --- |
|
|
| `active` | 재직 | 가능 | 생성/갱신 | 노출 |
|
|
| `temporary_leave` | 단기휴무 | 가능 | 계정 유지 | 노출 |
|
|
| `suspended` | 정지 | 불가 | suspend | 노출 |
|
|
| `preboarding` | 입사대기 | 불가 | 생성 안 함 | 비노출 |
|
|
| `baron_guest` | Baron 게스트 | 가능 | 생성 금지, 기존 계정 delete/deprovision | 비노출 |
|
|
| `extended_leave` | 장기휴직 | 불가 | delete/deprovision | 비노출 |
|
|
| `archived` | 보관 | 불가 | delete/deprovision | 비노출 |
|
|
|
|
- 기존 `inactive` 입력은 `preboarding`으로, `leave_of_absence` 입력은 `temporary_leave`로 호환 처리합니다.
|
|
- 이슈 #862의 초기 명칭 `baron_only`는 구현 명칭으로 사용하지 않고 `baron_guest`로 정리합니다.
|
|
- backend bootstrap은 남아 있는 legacy `users.status` 값을 `inactive -> preboarding`, `leave_of_absence -> temporary_leave`, `baron_only -> baron_guest`로 자동 정규화합니다.
|
|
- `archived` 사용자는 과거 이력 보존용 계정이며 AdminFront 같은 관리자 화면에서만 감사/운영/중복 확인 목적으로 조회할 수 있습니다.
|
|
|
|
|
|
### 4. 주요 시나리오 (Core Scenarios)
|
|
1. **Same Browser SSO**: Baron 로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인).
|
|
1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동
|
|
2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화
|
|
2.1 향후 App Push 등 2차 인증 강화수단 검토 필요
|
|
3. **QR Login**: 최초 진입 시 사전 로그인되어 있는 웹/앱을 이용해 QR 코드를 스캔하여, QR코드가 로딩된 Device를 로그인 상태로 전환
|
|
|
|
### 5. Headless Login ID/Password Flow
|
|
- 목적: headless login을 허용한 클라이언트가 자체 로그인 화면에서 `ID/password`를 수집하되, Baron Backend가 OIDC 로그인 흐름만 계속 진행하고 RP에는 `sessionJwt`를 직접 넘기지 않습니다.
|
|
- 대상 엔드포인트: `POST /api/v1/auth/headless/password/login`
|
|
- 관련 구현:
|
|
- `backend/internal/handler/auth_handler.go`
|
|
- `backend/internal/domain/hydra_models.go`
|
|
- `backend/internal/handler/auth_handler_login_test.go`
|
|
|
|
#### 호출 순서
|
|
1. RP 브라우저가 Hydra Public의 `/oauth2/auth`를 호출해 OIDC 인증을 시작합니다.
|
|
2. Hydra가 로그인 단계로 넘긴 `login_challenge`를 RP가 확보합니다.
|
|
3. RP backend가 자기 private key로 `client_assertion` JWT를 서명합니다.
|
|
4. RP backend가 Baron Backend의 `POST /api/v1/auth/headless/password/login`에 `client_id`, `client_assertion`, `login_challenge`, `loginId`, `password`를 전송합니다.
|
|
5. Baron Backend가 Hydra login request와 RP 설정을 검증한 뒤 Kratos sign-in 및 Hydra login accept를 수행합니다.
|
|
6. 성공 시 Baron Backend는 `redirectTo`만 반환하고, RP 브라우저는 그 URL로 이동해 OIDC 흐름을 이어갑니다.
|
|
|
|
#### 요청 바디
|
|
```json
|
|
{
|
|
"client_id": "headless-login-client",
|
|
"client_assertion": "<signed-jwt>",
|
|
"login_challenge": "<hydra-login-challenge>",
|
|
"loginId": "employee001",
|
|
"password": "secret"
|
|
}
|
|
```
|
|
|
|
#### 성공 응답
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"provider": "ory",
|
|
"redirectTo": "https://rp.example.com/callback?code=..."
|
|
}
|
|
```
|
|
|
|
#### RP / Hydra 선행 조건
|
|
- Hydra login request의 `client.client_id`와 요청 바디의 `client_id`가 반드시 같아야 합니다.
|
|
- client가 headless login 선행 조건을 만족해야 합니다.
|
|
- `headless_token_endpoint_auth_method == "private_key_jwt"` 또는 top-level `token_endpoint_auth_method == "private_key_jwt"`
|
|
- `headless_jwks_uri`가 존재해야 합니다.
|
|
- inline `headless_jwks`는 더 이상 지원하지 않습니다.
|
|
- `headless_login_enabled == true`가 필요합니다.
|
|
- `metadata.status == "inactive"`인 client는 차단됩니다.
|
|
|
|
#### `client_assertion` 규칙
|
|
- 구현상 `client_assertion`은 현재 필수입니다.
|
|
- 허용 서명 알고리즘:
|
|
- `RS256`, `RS384`, `RS512`
|
|
- `PS256`, `PS384`, `PS512`
|
|
- `ES256`, `ES384`, `ES512`
|
|
- `EdDSA`
|
|
- JWT claim의 `iss`와 `sub`는 모두 `client_id`와 같아야 합니다.
|
|
- `exp`는 현재 시각 이후여야 합니다.
|
|
- `nbf`, `iat`가 있으면 미래 시각이면 안 됩니다.
|
|
- `aud`는 다음 둘 중 하나와 일치해야 합니다.
|
|
- `https://<backend-origin>/api/v1/auth/headless/password/login`
|
|
- `/api/v1/auth/headless/password/login`
|
|
- 서명 검증용 public key는 `headless_jwks_uri`에서만 읽습니다.
|
|
|
|
#### 내부 JWKS 캐시 정책
|
|
- Baron Backend는 `headless_jwks_uri`를 직접 외부 스펙으로 저장하고, 실제 JWKS 문서는 내부 캐시에 저장해 사용합니다.
|
|
- 등록/수정 이후에는 내부 캐시 동기화를 시도하고, 성공/실패 상태를 DevFront에서 확인할 수 있습니다.
|
|
- 로그인 시 재조회는 다음 조건으로 제한합니다.
|
|
- 캐시에 `kid`가 없을 때
|
|
- `kid`는 있지만 서명 검증이 실패할 때
|
|
- 캐시 TTL이 만료되었을 때
|
|
- 그 외에는 내부 캐시를 사용합니다.
|
|
- 백그라운드 worker가 TTL보다 짧은 주기로 `jwksUri`를 선제 점검해 첫 사용자 실패를 줄입니다.
|
|
|
|
#### DevFront 운영 액션
|
|
- Settings > `Public Key Registration` 카드에서 다음 정보를 확인할 수 있습니다.
|
|
- 최근 JWKS 캐시 갱신 시각
|
|
- 최근 검증 성공 시각
|
|
- 최근 에러와 연속 실패 횟수
|
|
- 현재 cached `kid` 목록
|
|
- 파싱된 key summary
|
|
- `kid`
|
|
- `kty`
|
|
- `use`
|
|
- `alg`
|
|
- RSA key의 `n` preview (앞/뒤 일부만 표시)
|
|
- 수동 운영 액션:
|
|
- `Refresh JWKS Cache`
|
|
- `Revoke JWKS Cache`
|
|
- RP가 키를 교체했으면 실제 트래픽 전에 `Refresh JWKS Cache`를 먼저 호출하는 것을 권장합니다.
|
|
|
|
#### 일반 로그인과의 차이
|
|
- `POST /api/v1/auth/password/login`
|
|
- UserFront 기본 비밀번호 로그인용입니다.
|
|
- `login_challenge`가 없으면 `sessionJwt`를 반환합니다.
|
|
- `login_challenge`가 있으면 Hydra accept 후 `redirectTo`를 반환합니다.
|
|
- `POST /api/v1/auth/headless/password/login`
|
|
- headless login 허용 클라이언트 전용입니다.
|
|
- `client_assertion` 검증이 추가됩니다.
|
|
- 항상 `sessionJwt` 없이 `redirectTo`만 반환합니다.
|
|
|
|
#### 실패 패턴 요약
|
|
- `400 bad_request`
|
|
- 필수 필드 누락
|
|
- `client_assertion` 누락
|
|
- `401 invalid_client_assertion`
|
|
- `jwksUri` 조회 실패
|
|
- 서명 불일치
|
|
- `aud`/`iss`/`sub`/`exp` 검증 실패
|
|
- `401 invalid_client_assertion` with explicit message
|
|
- `Headless login requires jwksUri. Inline jwks is not supported.`
|
|
- `Configured jwksUri returned no keys for headless login.`
|
|
- `Failed to refresh headless login jwks from jwksUri.`
|
|
- `403 forbidden`
|
|
- `client_id` 불일치
|
|
- `headless_login_enabled` 미설정
|
|
- inactive client
|
|
- `401 password_or_email_mismatch`
|
|
- 사용자 인증 실패
|
|
|
|
|
|
### 전체 연결 구조도
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
subgraph Clients ["External Clients"]
|
|
AF[adminfront]
|
|
DF[devfront]
|
|
UF["userfront"]
|
|
DS["일반SW"]
|
|
end
|
|
|
|
subgraph AppService ["Control Plane"]
|
|
BE["Backend (Command/Audit Controller)"]
|
|
end
|
|
|
|
subgraph OryBundle ["Ory Deployment Stack"]
|
|
direction TB
|
|
OK["Oathkeeper (Public Proxy/OIDC)"]
|
|
|
|
subgraph OryEngines ["Ory Services"]
|
|
direction LR
|
|
HY["Hydra"]
|
|
KR["Kratos"]
|
|
KE["Keto"]
|
|
end
|
|
|
|
ICH[(Internal Clickhouse)]
|
|
|
|
%% Internal Flow within Bundle
|
|
OK -->|Routing/Queries| OryEngines
|
|
OK -.->|Access/Usage Log| ICH
|
|
end
|
|
|
|
subgraph AuditDB ["Audit Storage"]
|
|
ECH[(External Clickhouse)]
|
|
end
|
|
|
|
%% Key Command Path
|
|
AF & DF & UF & DS ==>|Actions / Commands| BE
|
|
|
|
%% Backend Responsibilities
|
|
BE -->|Admin/State Control| OryEngines
|
|
BE -.->|Mandatory Audit Log| ECH
|
|
|
|
%% Connection Note (Hidden flow mentioned in logic)
|
|
%% OK is technically the entry for OIDC, but removed as per request
|
|
|
|
%% Styles
|
|
style OryBundle fill:#f8f9fa,stroke:#333,stroke-width:2px
|
|
style BE fill:#bbf,stroke:#333,stroke-width:2px
|
|
style ECH fill:#fdd,stroke:#333
|
|
style ICH fill:#dfd,stroke:#333
|
|
style OK fill:#f9f,stroke:#333
|
|
style OryEngines fill:#fff,stroke:#999,stroke-dasharray: 5 5
|
|
```
|
|
|
|
|
|
Kratos가 사용자 SoT이며 Hydra는 순수 OIDC 토큰 엔진입니다. 비지니스로직은 Backend를 통해서, 기본 인증 로직은 Ory Stack을 통해 진행됩니다.
|
|
|
|
### SSOT 및 Redis Cache 전략
|
|
|
|
Baron SSO의 인증, 권한, OAuth/OIDC 원장은 Ory Stack입니다. Backend는 원장 쓰기 경로와 감사 로그를 중앙화하는 Control Plane입니다. 사용자 identity/profile/소속/조직도 노출 데이터에 대해 Backend DB `users`를 원장 또는 read model로 사용하지 않습니다. Redis는 Ory 원장 데이터의 성능 cache/mirror로만 사용합니다.
|
|
|
|
Ory에서 Redis cache로 웜업된 identity/조직 데이터는 frontend가 직접 소비하지 않습니다. Backend가 Redis mirror 또는 Ory Admin API fallback을 기준으로 cursor 기반 API를 adminfront, orgfront, userfront, 외부 API에 제공합니다.
|
|
|
|
#### 데이터별 원본 위치
|
|
|
|
| 데이터 | SSOT | 보조 저장소/캐시 | 비고 |
|
|
| --- | --- | --- | --- |
|
|
| Identity subject, credentials, recovery/verification address | Ory Kratos `identities` | Redis identity mirror | Kratos identity ID가 사용자 subject이며 WORKS `externalKey` 기준입니다. |
|
|
| 로그인 식별자 | Ory Kratos traits | Redis identity mirror/index | Kratos가 인증 식별자의 원장입니다. |
|
|
| 사용자 이름, 이메일, 전화번호, role 기본값 | Ory Kratos traits | Redis identity mirror | 인증/profile 계산에 필요한 identity 값은 Kratos 기준으로 유지합니다. |
|
|
| Baron 사용자 운영 상태, soft delete, 운영 메타데이터 | Ory Kratos traits/state 또는 별도 명시 원장 | Redis mirror/cache | Backend DB `users`를 사용자 read model로 사용하지 않습니다. |
|
|
| 테넌트 tree, slug, 조직/부서/직무/직책 | Ory Keto relation tuple, Backend read model | Redis/API response cache 가능 | 권한/관계 판단은 Keto가 원장입니다. Ory가 보관하거나 조회할 수 없는 조직 표시/검색 데이터만 Backend read model에 둡니다. |
|
|
| 권한/관계 | Ory Keto relation tuple | PostgreSQL outbox/status | Backend를 통해 relation command를 보내고 처리 상태를 추적합니다. |
|
|
| OAuth2/OIDC client, consent, token state | Ory Hydra | PostgreSQL `client_consents`, audit/read model | Hydra가 프로토콜 원장이며 로컬 테이블은 운영 조회/감사용입니다. |
|
|
| RP별 사용자 custom claim 값 | Backend read model `rp_user_metadata` | ID token/userinfo claim assembly | Ory에 저장되지 않는 RP 범위 데이터입니다. Kratos traits나 claim output을 SSOT로 취급하지 않습니다. |
|
|
| 전역 사용자 custom claim 값 | Backend read model `users.metadata.global_custom_claims` | ID token claim assembly | Ory에 저장되지 않는 운영 범위 custom 값입니다. |
|
|
| WORKS Mobile mapping/outbox/job 상태 | PostgreSQL `worksmobile_*` | WORKS API 비교 응답 cache 가능 | 외부 SaaS 연동 상태이며 identity 원장이 아닙니다. |
|
|
| 감사 로그/사용량 | ClickHouse, Oathkeeper/Ory 로그 | 화면별 summary cache 가능 | command와 보안 이벤트의 감사 원장입니다. |
|
|
| Headless JWKS 검증 상태 | Redis `headless:jwks:*` cache | DevFront 상태 카드 | RP public key 문서 자체는 외부 `jwksUri`가 원본입니다. |
|
|
| 로그인 코드, pending login, verification token | Redis short-lived key | 없음 | 만료 가능한 휘발성 상태입니다. 백업/복구 대상이 아닙니다. |
|
|
|
|
#### SSOT 보장 원칙
|
|
|
|
1. Kratos/Hydra/Keto/WORKS로 향하는 쓰기 command는 Backend를 통과합니다.
|
|
2. Backend는 Ory write 성공 후 원장 ID를 기준으로 Ory를 재조회하고, Redis mirror를 갱신하거나 stale로 표시합니다. 사용자 identity/profile/소속 데이터는 Backend DB `users`에 read model로 갱신하지 않습니다.
|
|
3. write-through 갱신 실패 시 원장 write를 되돌린 것으로 간주하지 않습니다. 대신 mirror/cache 상태를 `stale` 또는 `failed`로 표시하고 drift report와 refresh 대상으로 둡니다.
|
|
4. Kratos Admin API 또는 Kratos DB를 Backend 밖에서 직접 수정하는 경로는 운영 정책상 금지합니다. 정비/DR처럼 예외가 필요한 경우에는 Redis mirror를 stale로 표시하고, full refresh와 drift report를 완료하기 전까지 cache 결과를 신뢰하지 않습니다.
|
|
5. Backend DB `users`나 Redis cache는 Kratos partial list를 full snapshot처럼 취급하지 않습니다. Kratos 목록 조회가 partial이면 로컬 사용자 데이터를 근거로 정상 목록을 만들지 않습니다.
|
|
6. frontend/API 대량 조회는 Backend가 제공하는 cursor 기반을 원칙으로 합니다. `limit=5000&offset=0` 같은 단일 대량 offset 조회는 사용자 수가 늘면 partial data를 전체처럼 보이게 만들 수 있으므로 신규 구현에서 금지합니다.
|
|
7. Redis cache miss가 발생한 단건 조회는 가능한 경우 SSOT로 fallback하고, fallback 성공 시 Redis를 갱신합니다. 목록 조회는 mirror 상태가 `ready`가 아니면 화면/API에 경고 상태를 함께 전달해야 합니다.
|
|
|
|
#### Redis 사용 원칙
|
|
|
|
Redis는 원장이 아니라 cache/mirror 계층입니다. Redis 데이터 유실은 장애지만 데이터 유실 사고로 보지 않고, 원장 재조회와 refresh로 재수렴해야 합니다.
|
|
|
|
| Redis 데이터 | 역할 | TTL/보존 정책 | 장애 시 처리 |
|
|
| --- | --- | --- | --- |
|
|
| `identity:mirror:{identityID}` | Kratos identity summary 단건 cache | 장기 mirror. refresh 상태와 함께 운영 | Kratos `GetIdentity` fallback 후 write-through |
|
|
| `identity:index:*` | Backend cursor API용 identity 목록/검색 index | mirror refresh 주기로 재작성 | `stale` 표시 후 full refresh |
|
|
| `identity:mirror:state` | mirror 상태, count, last error | 영구 상태 key | adminfront에서 경고 표시 |
|
|
| `headless:jwks:*` | RP headless login JWKS cache | JWKS TTL과 prefetch 정책 | kid miss/검증 실패/TTL 만료 시 재조회 |
|
|
| login/verification/pending 계열 key | 인증 흐름의 단기 상태 | 짧은 TTL 필수 | 만료 또는 유실 시 사용자가 흐름 재시작 |
|
|
| 일반 API response cache | 선택적 성능 cache | 짧은 TTL, invalidation 우선 | miss 시 Backend DB 또는 Ory 원장 조회 |
|
|
|
|
운영 Redis 설정은 `maxmemory`와 `maxmemory_policy`가 명시되어야 합니다. identity mirror처럼 재수렴 가능한 데이터와 pending login처럼 사용자 흐름에 영향을 주는 단기 key가 같은 Redis를 공유하므로, eviction 발생 여부와 TTL 없는 key 증가를 운영 화면에서 볼 수 있어야 합니다.
|
|
|
|
#### Redis 모니터링 계획
|
|
|
|
Redis 적정 설정 판단에 필요한 운영 지표를 adminfront에 노출하는 후속 작업은 이슈 [#1046](https://gitea.hmac.kr/baron/baron-sso/issues/1046)으로 분리했습니다.
|
|
|
|
표시 대상은 Redis 연결/버전/uptime, `used_memory`, `maxmemory`, `maxmemory_policy`, keyspace hit/miss, expired/evicted keys, prefix별 key count, TTL 분포, `identity:mirror:state`, headless JWKS cache failure 요약입니다. 이 화면은 `super_admin` 전용으로 두고, Redis key value 자체는 노출하지 않습니다.
|
|
|
|
---
|
|
|
|
## 🚀 시작하기 (Getting Started)
|
|
|
|
### 사전 요구사항 (Prerequisites)
|
|
- Docker & Docker Compose
|
|
- Flutter SDK (로컬 개발용, 3.38.0+)
|
|
- Go (로컬 백엔드 개발용)
|
|
|
|
### 환경 설정 (Environment Setup)
|
|
1. 예제 환경 설정 파일을 복사합니다.
|
|
```bash
|
|
cp .env.sample .env
|
|
```
|
|
2. `.env`를 작성합니다. (아래 작성 규칙 필수)
|
|
|
|
### `.env` 작성 규칙 (중요)
|
|
- `KEY=value` 한 줄만 사용하고, **값 뒤에 같은 줄 주석을 붙이지 않습니다.**
|
|
- 주석이 필요하면 반드시 **윗줄에 별도 주석 라인**으로 작성합니다.
|
|
- URL 값 끝에 공백이 들어가면 Hydra/Kratos 기동 실패로 이어질 수 있습니다.
|
|
|
|
잘못된 예:
|
|
```env
|
|
USERFRONT_URL=https://sso.example.com # 이렇게 같은 줄 주석 금지
|
|
```
|
|
|
|
올바른 예:
|
|
```env
|
|
# UserFront 공개 URL
|
|
USERFRONT_URL=https://sso.example.com
|
|
```
|
|
|
|
### `.env` 핵심 변수 가이드
|
|
- `IDP_PROVIDER`: 기본 `ory`
|
|
- `USERFRONT_URL`: 브라우저 기준 공개 도메인 (예: `https://sso.example.com`)
|
|
- `OATHKEEPER_PUBLIC_URL`: 보통 `${USERFRONT_URL}`
|
|
- `HYDRA_PUBLIC_URL`: 보통 `${OATHKEEPER_PUBLIC_URL}/oidc`
|
|
- `KRATOS_BROWSER_URL`: 보통 `${OATHKEEPER_PUBLIC_URL}/auth`
|
|
- `KRATOS_UI_URL`: UserFront UI URL (로컬 예: `http://localhost:5000`)
|
|
- `ADMINFRONT_CALLBACK_URLS`: 콤마 구분 콜백 목록 (예: `http://localhost:5173/auth/callback`)
|
|
- `DEVFRONT_CALLBACK_URLS`: 콤마 구분 콜백 목록 (예: `http://localhost:5174/auth/callback`)
|
|
- 주의: callback URL 끝에 `/`가 붙으면 `make validate-auth-config`에서 실패 처리됩니다.
|
|
- `KRATOS_ALLOWED_RETURN_URLS_EXTRA`: 추가 허용 return URL (선택)
|
|
- 빈값: `[]`
|
|
- 다중값: `["https://a.example.com/callback","https://b.example.com/callback"]` 또는 `https://a.example.com/callback,https://b.example.com/callback`
|
|
- `KRATOS_ALLOWED_RETURN_URLS_JSON`: stage/prod에서 권장하는 전체 허용 return URL 목록
|
|
- 공개 도메인, `/ko`, `/en`, `/auth/callback`, `/ko/auth/callback`, `/en/auth/callback`, 각 front callback을 포함해야 합니다.
|
|
- `CLIENT_LOG_DEBUG`: 클라이언트 로그 디버그 모드 강제 (기본: 비운영 `true`, 운영 `false`)
|
|
- 운영(`APP_ENV=production|prod`)에서 `true|1|on|yes` 설정 시 `INFO/DEBUG` 클라이언트 로그 수집 허용
|
|
- 미설정(기본) 시 운영에서는 `WARN/ERROR`만 수집
|
|
- `BACKEND_LOG_LEVEL`: Backend `slog` 레벨 override (선택)
|
|
- 허용 값: `debug`, `info`, `warn`, `error`
|
|
- 미설정 시 `APP_ENV` 기준으로 결정됩니다.
|
|
- `dev|local|development`: `debug`
|
|
- 그 외(`stage`, `production`, `prod` 등): `info`
|
|
- `USERFRONT_DEBUG_LOG`: UserFront 측 디버그 로그 fallback 플래그
|
|
- `CLIENT_LOG_DEBUG`가 없을 때만 UserFront에서 대체로 참조
|
|
|
|
### 클라이언트 로그 정책 (중요)
|
|
- 기본 원칙: 운영 환경에서는 민감정보 보호를 우선하며, 과도한 로그 수집을 제한합니다.
|
|
- 환경별 동작:
|
|
- 비운영(`dev/local/stage` 등): 디버그 로그 허용 (`INFO/DEBUG/WARN/ERROR`)
|
|
- 운영(`production/prod`) + `CLIENT_LOG_DEBUG` 미설정: `WARN/ERROR`만 수집
|
|
- 운영(`production/prod`) + `CLIENT_LOG_DEBUG=true`: 디버그 로그 허용
|
|
- 민감정보는 환경과 무관하게 마스킹합니다.
|
|
- 예: `password`, `newPassword`, `token`, `authorization`, `cookie`, `sessionJwt`
|
|
- 문자열 패턴(`token=...`, `authorization:...`, JSON body 내 민감 key)도 마스킹
|
|
- 상세 정책 문서: `docs/client-log-policy.md`
|
|
|
|
### Backend 로그 정책 (중요)
|
|
- Backend 서버 로그는 `APP_ENV` 기준으로 기본 레벨이 정해집니다.
|
|
- `dev|local|development`: `debug`
|
|
- 그 외(`stage`, `production`, `prod` 등): `info`
|
|
- 운영/스테이징에서 장애 분석이 필요할 때만 `BACKEND_LOG_LEVEL=debug`를 일시적으로 설정하는 것을 권장합니다.
|
|
- headless login 같은 경로의 상세 진단 필드는 `debug` 레벨에서만 추가로 남습니다.
|
|
- 상세 정책 문서: `docs/backend-log-policy.md`
|
|
|
|
### `.env` 작성 후 권장 점검
|
|
```bash
|
|
make validate-auth-config
|
|
```
|
|
위 검증은 callback/allowed_return_urls/게이트웨이 매핑 규칙을 점검하고 `config/.generated/auth-config.env`를 생성합니다.
|
|
|
|
### 전체 스택 실행 (Running the Stack)
|
|
|
|
#### 1. 네트워크 생성 (최초 1회)
|
|
Ory Stack과 애플리케이션 간 통신을 위한 도커 네트워크를 생성합니다.
|
|
```bash
|
|
# ory-net은 bridge 모드로 생성
|
|
docker network create -d bridge ory-net
|
|
docker network create hydranet
|
|
docker network create kratosnet
|
|
docker network create public_net #서비스용
|
|
```
|
|
|
|
#### 2. 인프라 및 Ory Stack 실행
|
|
데이터베이스와 Ory 서비스(Kratos, Hydra, Keto 등)를 실행합니다.
|
|
```bash
|
|
# 권장: Make 실행 (인증 설정 검증 포함)
|
|
make up-dev
|
|
```
|
|
|
|
#### 3. 애플리케이션 실행
|
|
userfront와 backend 서비스를 실행합니다.
|
|
```bash
|
|
make up-app
|
|
```
|
|
(또는 전체 스택 한번에 실행: `make up-all`)
|
|
|
|
### Make 기반 인증 설정 검증 (권장)
|
|
`up-*` 타깃은 실행 전 인증 리다이렉트 설정을 자동 검증합니다.
|
|
|
|
```bash
|
|
# 1) 인증 설정 생성
|
|
make build-auth-config
|
|
|
|
# 2) 정적 검증 (callback / allowed_return_urls / 게이트웨이 매핑)
|
|
make validate-auth-config
|
|
|
|
# 3) 배선 + (가능 시) 런타임 Hydra client 검증
|
|
make verify-auth-config
|
|
```
|
|
|
|
- 생성 파일: `config/.generated/auth-config.env` (compose 실행 시 자동 주입)
|
|
- 게이트웨이 경유 환경은 URL 문자열 완전일치 대신 매핑 유효성(`direct_match` / `mapped_match`) 기준으로 검증합니다.
|
|
- 관련 정책 문서: `docs/oidc_redirect_mapping_validation_policy.md`
|
|
|
|
### 권장 실행 순서
|
|
```bash
|
|
cp .env.sample .env
|
|
# .env 편집
|
|
make validate-auth-config
|
|
make up-dev
|
|
make up-app
|
|
```
|
|
|
|
직접 Compose를 사용하려면 다음처럼 env 파일을 함께 주입하세요.
|
|
```bash
|
|
docker compose --env-file .env --env-file config/.generated/auth-config.env -f compose.infra.yaml -f compose.ory.yaml up -d
|
|
docker compose --env-file .env --env-file config/.generated/auth-config.env -f docker-compose.yaml up -d
|
|
```
|
|
|
|
- **gateway (UserFront 프록시)**: http://localhost:5000 접속
|
|
- **backend**: http://localhost:3000 (API)
|
|
- **ClickHouse**: http://localhost:8123
|
|
- **Kratos Public**: http://localhost:4433
|
|
- **Hydra Public**: http://localhost:4444
|
|
- **Kratos UI (UserFront)**: http://localhost:5000
|
|
|
|
### 전체 백업/복구
|
|
|
|
전체 백업/복구는 CSV export/import가 아니라 Baron SSO와 Ory Stack 저장소를 같은 시점의 재해 복구 단위로 보존하는 절차입니다. 사용자 UUID, Kratos identity ID, Hydra/Keto 원장, WORKS 연동 mapping이 어긋나면 안 되므로 운영 복구는 DB dump와 설정 snapshot을 함께 다룹니다.
|
|
|
|
#### 백업 실행
|
|
```bash
|
|
# 전체 백업
|
|
make dump
|
|
|
|
# 출력 위치를 직접 지정
|
|
make dump BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ
|
|
|
|
# 일부 서비스만 백업
|
|
make dump DUMP_SERVICES=postgres,ory-postgres,clickhouse,ory-clickhouse,config
|
|
make dump DUMP_SERVICES=ory-postgres,ory-clickhouse
|
|
|
|
# 생성된 백업 검증
|
|
make dump-verify BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ
|
|
|
|
# WORKS Drive로 외부 분산 저장
|
|
make upload-cloud BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ
|
|
|
|
# 지정 경로로 dump 후 바로 WORKS Drive 업로드
|
|
make dump-upload-cloud BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ
|
|
|
|
# 로컬 백업 목록
|
|
make dump-list
|
|
```
|
|
|
|
기본값은 `DUMP_SERVICES=all`, `DUMP_MODE=maintenance`입니다. `DUMP_SERVICES`는 다음 값을 콤마로 조합할 수 있습니다.
|
|
|
|
| 값 | 대상 |
|
|
| --- | --- |
|
|
| `postgres` | Baron Postgres (`baron_postgres`, `${DB_NAME:-baron_sso}`) |
|
|
| `ory-postgres` | Ory Postgres의 `${KRATOS_DB:-ory_kratos}`, `${HYDRA_DB:-ory_hydra}`, `${KETO_DB:-ory_keto}` |
|
|
| `clickhouse` | Baron ClickHouse (`baron_clickhouse`) |
|
|
| `ory-clickhouse` | Ory ClickHouse (`ory_clickhouse`) |
|
|
| `config` | `.env` redacted copy, generated Ory config, gateway, 주요 compose 파일 |
|
|
|
|
백업 산출물은 기본적으로 `backups/baron-sso-backup-YYYYMMDD-HHMMSSZ/` 아래에 생성됩니다.
|
|
|
|
```text
|
|
manifest.json
|
|
checksums.sha256
|
|
postgres/
|
|
clickhouse/
|
|
config/
|
|
reports/
|
|
```
|
|
|
|
#### WORKS Drive 외부 업로드
|
|
|
|
`make dump`, `make restore`, `make upload-cloud`는 기본적으로 `docker/backup-tools/Dockerfile`에서 빌드한 `baron-sso-backup-tools:local` 컨테이너 안에서 실행됩니다. 호스트에는 Docker와 Docker socket 접근 권한만 필요하고, `zstd`, `jq`, `curl`, `openssl`, `postgresql-client` 같은 백업/복구 도구는 backup-tools image에 포함됩니다.
|
|
|
|
`make upload-cloud`는 기존 백업 디렉터리를 `baron-sso-backup-*.tar.zst`로 묶은 뒤 WORKS Drive에 업로드합니다. 압축 포맷은 `.tar.zst`로 고정되어 있고, 압축/해제는 backup-tools 컨테이너 내부의 `zstd`로 수행합니다.
|
|
|
|
백업이 완료되면 `reports/backup-report.md`도 생성됩니다. 이 report에는 사용자 수, 테넌트 수, RP 수, Hydra client 수, WORKS 관련 row count, 서비스별 수행 시간이 Markdown 표로 기록됩니다. `make upload-cloud`는 `reports/*.md`만 WORKS Drive 대상 폴더 아래의 `reports` 하위 폴더로 업로드하며, 업로드 파일명은 `backup-report-YYYYMMDD-HHMMSSZ.md`처럼 업로드 시각을 붙입니다. `reports/cloud-upload.json`은 로컬 업로드 실행 기록으로만 남기고 Drive에는 업로드하지 않습니다.
|
|
|
|
```bash
|
|
# 권장: 백업 경로를 명시해서 dump와 upload를 분리
|
|
make dump BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ
|
|
make upload-cloud BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ
|
|
|
|
# 또는 같은 BACKUP 경로로 연속 실행
|
|
make dump-upload-cloud BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ
|
|
|
|
# 실제 업로드 전 endpoint와 target만 확인
|
|
make upload-cloud BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ WORKS_DRIVE_DRY_RUN=true
|
|
|
|
# 예외적으로 호스트 도구로 직접 실행
|
|
make restore BACKUP_USE_DOCKER=false BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ CONFIRM_RESTORE=baron-sso
|
|
```
|
|
|
|
주요 변수:
|
|
|
|
| 변수 | 설명 |
|
|
| --- | --- |
|
|
| `WORKS_DRIVE_TARGET` | `sharedrive`, `mydrive`, `group`, `sharedfolder` 중 하나. 기본값은 `sharedrive`입니다. |
|
|
| `WORKS_DRIVE_SHARED_DRIVE_ID` | `WORKS_DRIVE_TARGET=sharedrive`일 때 공용 드라이브 ID입니다. |
|
|
| `WORKS_DRIVE_PARENT_FILE_ID` | 업로드할 대상 폴더의 WORKS Drive `fileId`입니다. 폴더 이름이나 경로가 아니며, 비우면 대상 drive/folder root에 업로드합니다. |
|
|
| `WORKS_DRIVE_USER_ID` | `mydrive` 또는 `sharedfolder` 대상 사용자 ID입니다. 기본값은 `me`입니다. |
|
|
| `WORKS_DRIVE_GROUP_ID` | `WORKS_DRIVE_TARGET=group`일 때 조직/그룹 ID입니다. |
|
|
| `WORKS_DRIVE_SHARED_FOLDER_ID` | `WORKS_DRIVE_TARGET=sharedfolder`일 때 공유받은 폴더 ID입니다. |
|
|
| `WORKS_DRIVE_ACCESS_TOKEN` | Drive API 호출용 Bearer token입니다. Drive API는 `file` scope가 필요합니다. |
|
|
| `WORKS_DRIVE_ACCESS_TOKEN_FILE` | access token을 파일에서 읽을 때 사용합니다. |
|
|
| `WORKS_DRIVE_ACCESS_TOKEN_CMD` | access token을 명령 출력으로 주입할 때 사용합니다. |
|
|
| `WORKS_DRIVE_OAUTH_SCOPE` | Drive 업로드 앱 OAuth token에 사용할 scope입니다. 기본값은 `file`입니다. |
|
|
| `WORKS_DRIVE_OAUTH_CLIENT_ID` | Drive 업로드 앱의 OAuth client ID입니다. 계정 동기화용 `WORKS_ADMIN_OAUTH_CLIENT_ID`와 분리합니다. |
|
|
| `WORKS_DRIVE_OAUTH_CLIENT_SECRET` | Drive 업로드 앱의 OAuth client secret입니다. |
|
|
| `WORKS_DRIVE_OAUTH_REFRESH_TOKEN` | Drive 업로드 앱의 refresh token입니다. 명시 access token이 없으면 이 값으로 access token을 갱신합니다. |
|
|
| `WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT` | Drive 업로드 앱의 service account입니다. JWT `sub`에 들어갑니다. |
|
|
| `WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE` | Drive 업로드 앱 private key 파일입니다. 예: `./config/worksmobile-driveapp-private-key.pem` |
|
|
| `WORKS_DRIVE_SPLIT_SIZE` | 분할 업로드 시 part 크기입니다. 기본값은 `9000M`입니다. |
|
|
| `WORKS_DRIVE_MAX_SINGLE_FILE_BYTES` | 이 값보다 archive가 크면 split part로 나눕니다. 기본값 `0`은 자동 분할 비활성입니다. |
|
|
| `WORKS_DRIVE_FORCE_SPLIT` | `true`이면 크기와 무관하게 split part로 업로드합니다. |
|
|
| `WORKS_DRIVE_OVERWRITE` | WORKS Drive upload URL 생성 요청의 overwrite 플래그입니다. 기본값은 `false`입니다. |
|
|
| `WORKS_DRIVE_UPLOAD_REPORTS` | `true`이면 `reports/*.md`를 Drive의 report 폴더로 함께 업로드합니다. 기본값은 `true`입니다. |
|
|
| `WORKS_DRIVE_REPORT_FOLDER_NAME` | Markdown report를 업로드할 하위 폴더 이름입니다. 기본값은 `reports`입니다. |
|
|
|
|
Drive API는 업로드 URL 생성 후 해당 URL에 multipart `Filedata`로 실제 파일을 전송하는 2단계 방식입니다. 계정 동기화용 `WORKS_ADMIN_OAUTH_*`와 Drive 업로드용 `WORKS_DRIVE_OAUTH_*`는 서로 다른 앱/키로 관리합니다. token 우선순위는 `WORKS_DRIVE_ACCESS_TOKEN`, `WORKS_DRIVE_ACCESS_TOKEN_FILE`, `WORKS_DRIVE_ACCESS_TOKEN_CMD`, `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`, 서비스 계정 JWT fallback 순서입니다. 운영에서는 Drive API 권한과 `file` scope 위임 정책을 먼저 확인해야 합니다.
|
|
|
|
#### 복구 계획과 복구 실행
|
|
```bash
|
|
# 복구 전 계획 확인
|
|
make restore-plan BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ \
|
|
RESTORE_SERVICES=postgres,ory-postgres,clickhouse,ory-clickhouse,config \
|
|
CONFIRM_RESTORE=baron-sso
|
|
|
|
# 복구 실행
|
|
make restore BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ \
|
|
RESTORE_SERVICES=postgres,ory-postgres,clickhouse,ory-clickhouse,config \
|
|
CONFIRM_RESTORE=baron-sso
|
|
|
|
# .tar.zst archive를 직접 복구 입력으로 사용
|
|
make restore DUMP_FILE=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ.tar.zst \
|
|
RESTORE_SERVICES=all \
|
|
CONFIRM_RESTORE=baron-sso
|
|
|
|
# report 경로를 명시
|
|
make restore BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ \
|
|
CONFIRM_RESTORE=baron-sso \
|
|
RESTORE_REPORT=reports/restore/baron-sso-restore-report.json
|
|
|
|
# 복구 후 기본 검증
|
|
make restore-verify BACKUP=backups/baron-sso-backup-YYYYMMDD-HHMMSSZ
|
|
```
|
|
|
|
복구는 반드시 빈 volume 또는 restore 전용 stack에서 수행하는 것을 기본 정책으로 합니다. `make restore`는 `BACKUP` 또는 `DUMP_FILE` 중 하나와 `CONFIRM_RESTORE=baron-sso`가 없으면 실패하고, 기본적으로 non-empty Postgres 대상에는 복구하지 않습니다. 승인된 restore rehearsal에서만 `ALLOW_NON_EMPTY_RESTORE=true`를 사용하세요. `DUMP_FILE=.tar.zst` 해제도 backup-tools 컨테이너에서 수행하므로 호스트 `zstd` 설치에 의존하지 않습니다.
|
|
|
|
`make restore`는 복구 report를 JSON과 Markdown으로 남깁니다. `BACKUP` 디렉터리 입력의 기본 JSON report는 `<BACKUP>/reports/restore-report.json`이고, `DUMP_FILE` archive 입력의 기본 JSON report는 `reports/restore/<archive-name>-restore-report.json`입니다. 같은 경로에 `.md` 확장자의 Markdown 요약도 함께 생성됩니다. `RESTORE_REPORT`로 직접 지정할 수 있습니다. report에는 입력 archive, 복구 서비스, checksum 검증 상태, 복구 후 대상 row count 비교 결과가 기록됩니다.
|
|
|
|
`config` 복구는 운영 파일을 직접 덮어쓰지 않고 `config-restored/`에 풀어 수동 검토하도록 합니다. migration은 자동 실행하지 않으며, Ory Stack과 backend 기동 후 super admin login, 대표 OIDC login, WORKS comparison dry-run을 통과하기 전까지 WORKS relay를 자동 재개하지 않습니다.
|
|
|
|
#### 백업/복구 범위
|
|
|
|
필수 백업 대상:
|
|
- Baron Postgres: users, tenants, user_login_ids, user_groups, RP metadata, WORKS mapping/outbox 등
|
|
- Ory Postgres: Kratos identity/credentials/session, Hydra client/consent/token state, Keto relation tuple
|
|
- Baron ClickHouse: 감사 로그와 RP usage event
|
|
- Ory ClickHouse: Oathkeeper/Ory 계열 접근 로그
|
|
- 설정 snapshot: `.env` redacted copy, generated Ory config, gateway, compose 파일
|
|
|
|
기본 제외 대상:
|
|
- Redis: pending login, short code, cache 등 휘발성 데이터이므로 복구 후 재수렴 대상으로 봅니다.
|
|
- 프론트 빌드 산출물: 소스와 이미지 태그로 재생성합니다.
|
|
- coverage, reports, test-results 같은 로컬 개발 산출물
|
|
|
|
상세 설계와 운영 정책은 `docs/backup-restore-design.md`를 기준으로 유지합니다.
|
|
|
|
### MCP 서버 (Hydra/Kratos/Keto)
|
|
MCP 서버는 기존 Hydra/Kratos에 연결하며 별도 Ory 스택이나 포트를 추가로 띄우지 않습니다.
|
|
프로덕션에서는 실행하지 않도록 `mcp` 프로파일을 로컬에서만 켜세요.
|
|
|
|
```bash
|
|
docker compose -f compose.ory.yaml --profile mcp up -d hydra-mcp-server kratos-mcp-server keto-mcp-server
|
|
```
|
|
|
|
- MCP 서버는 stdio 기반이라 외부 포트를 열지 않습니다.
|
|
- 최초 실행시거나 빌드된 이미지가 없으면 `docker compose -f mcp/compose.mcp.ory.yaml build' 후에 사용 가능합니다
|
|
|
|
```toml
|
|
[mcp_servers.kratos-mcp]
|
|
command = "docker"
|
|
args = ["compose", "-f", "mcp/compose.mcp.ory.yaml", "run", "--rm", "--no-deps", "kratos-mcp-server"]
|
|
|
|
[mcp_servers.kratos-mcp.env]
|
|
KRATOS_ADMIN_URL = "http://kratos:4434"
|
|
|
|
[mcp_servers.hydra-mcp]
|
|
command = "docker"
|
|
args = ["compose", "-f", "mcp/compose.mcp.ory.yaml", "run", "--rm", "--no-deps", "hydra-mcp-server"]
|
|
|
|
[mcp_servers.hydra-mcp.env]
|
|
HYDRA_PUBLIC_URL = "http://hydra:4444"
|
|
HYDRA_ADMIN_URL = "http://hydra:4445"
|
|
|
|
[mcp_servers.keto-mcp]
|
|
command = "docker"
|
|
args = ["compose", "-f", "mcp/compose.mcp.ory.yaml", "run", "--rm", "--no-deps", "keto-mcp-server"]
|
|
|
|
[mcp_servers.keto-mcp.env]
|
|
KETO_READ_URL = "http://keto:4466"
|
|
KETO_WRITE_URL = "http://keto:4467"
|
|
```
|
|
|
|
## 🌐 i18n 구조 (간략)
|
|
- **Root locales**: `locales/template.toml`, `locales/ko.toml`, `locales/en.toml`은 현재 `userfront`와 전역 i18n 검증 기준 리소스입니다.
|
|
- **Common locales**: `common/locales/template.toml`, `common/locales/ko.toml`, `common/locales/en.toml`은 `ui.common.*`, `msg.common.*` 같은 React 공통 문구 레이어입니다.
|
|
- **React(Admin/Dev/Org)**: `adminfront/src/lib/i18n.ts`, `devfront/src/lib/i18n.ts`, `orgfront/src/lib/i18n.ts`에서 `t(key, fallback, vars)`를 사용하며 `common locale -> app locale override` 순서로 TOML을 `?raw` 로드합니다.
|
|
- **Flutter(User)**: `userfront/lib/i18n.dart`에서 `tr(key, fallback, params)` 사용. `locales/*.toml`을 `tools/i18n-scanner/gen-flutter-i18n.js`로 `userfront/lib/i18n_data.dart`에 사전 생성합니다.
|
|
- **UserFront 동기화 규칙**: `locales/*.toml`을 수정한 뒤에는 반드시 `./scripts/sync_userfront_locales.sh`를 실행해 `userfront/assets/translations/*.toml`과 런타임 번역 리소스를 동기화합니다.
|
|
- **검증**: `node tools/i18n-scanner/index.js`로 `root locales`와 `common/locales`의 코드-키-로케일 동기화 상태를 함께 점검합니다.
|
|
|
|
## 🧪 Code Check CI
|
|
워크플로우 파일: `.gitea/workflows/code_check.yml`
|
|
|
|
### 트리거
|
|
- `push` (`dev` 브랜치)
|
|
- `pull_request` (`dev` 대상)
|
|
- `workflow_dispatch` (수동 실행)
|
|
|
|
### workflow_dispatch 입력값
|
|
- `run_lint`: Go/Flutter lint 실행 여부
|
|
- `run_backend_tests`: backend 테스트 실행 여부
|
|
- `run_userfront_tests`: userfront 테스트 실행 여부
|
|
- `run_userfront_e2e_tests`: userfront WASM Playwright E2E 실행 여부
|
|
- `run_adminfront_tests`: adminfront 테스트 실행 여부
|
|
- `run_devfront_tests`: devfront 테스트 실행 여부
|
|
|
|
### 실행 잡
|
|
- `lint`
|
|
- `backend-tests`
|
|
- `userfront-tests`
|
|
- `userfront-e2e-tests`
|
|
- `adminfront-tests`
|
|
- `devfront-tests`
|
|
|
|
### 프런트 테스트 브라우저 프리비저닝 정책
|
|
- `userfront-tests`
|
|
- `flutter test`(VM)만 실행
|
|
- `locale_storage` 정책 테스트는 엔진 단위로 통합되어 별도 브라우저 실행이 필요하지 않음
|
|
- `adminfront-tests`, `devfront-tests`
|
|
- Playwright 기반 테스트
|
|
- `npx playwright install --with-deps`로 브라우저/OS 의존성을 사전 설치
|
|
|
|
### 실패 보고서 확인 방법
|
|
테스트가 실패하면 다음이 자동 생성됩니다.
|
|
- Job Summary: 실패 원인 요약(Markdown) 즉시 확인
|
|
- Artifact: 상세 로그/리포트 다운로드
|
|
- `backend-test-failure-report`
|
|
- `userfront-test-failure-report`
|
|
- `adminfront-test-failure-report`
|
|
- `devfront-test-failure-report`
|
|
|
|
### userfront `locale_storage` 테스트 정책
|
|
- `locale_storage_platform_test.dart`는 `LocaleStorageEngine` 기반 정책 테스트로 통합되었습니다.
|
|
- 일반 `flutter test`(VM) 실행에 포함되며, 브라우저 전용 `kIsWeb` 케이스를 사용하지 않습니다.
|
|
- 단일 파일만 확인하려면 다음 명령을 사용합니다.
|
|
- `flutter test test/locale_storage_platform_test.dart`
|
|
|
|
### userfront WASM Playwright E2E
|
|
- 워크스페이스: `userfront-e2e/`
|
|
- 빌드+실행:
|
|
- `cd userfront-e2e && npm run test:wasm`
|
|
- 빌드 결과가 이미 있을 때:
|
|
- `cd userfront-e2e && npm test`
|
|
- Makefile 타깃:
|
|
- `make code-check-userfront-e2e-tests`
|
|
- 전수 인벤토리:
|
|
- `https://gitea.hmac.kr/baron/baron-sso/wiki/UserFront-WASM-E2E-Inventory.-`
|
|
|
|
### 로컬 개발 (Manual)
|
|
Docker 없이는 개발할 수 없지만 Backend 및 [user/admin/dev]Front 코드는 개발모드로 수정하며 개발가능.
|
|
백그라운드로 infra 및 ory stack이 구동중이라는 가정
|
|
|
|
**Backend:**
|
|
```bash
|
|
cd backend
|
|
go mod tidy
|
|
go run cmd/server/main.go
|
|
```
|
|
|
|
**userfront:**
|
|
```bash
|
|
cd userfront
|
|
flutter pub get
|
|
flutter run -d chrome
|
|
# 정책: 웹 빌드는 기본적으로 WASM 사용
|
|
flutter build web --wasm
|
|
```
|
|
|
|
**adminfront:**
|
|
```bash
|
|
cd adminfront
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
**devfront:**
|
|
```bash
|
|
cd devfront
|
|
npm install
|
|
npm run dev
|
|
```
|
|
---
|
|
|
|
|
|
## 버그 대응 대원칙 (필수)
|
|
- 모든 버그 수정은 반드시 **재현 테스트를 먼저 작성**합니다. (Failing test first)
|
|
- 재현 테스트 없이 코드만 먼저 고치는 행위를 금지합니다.
|
|
- 수정 후에는 **해당 재현 테스트가 통과할 때까지 반복**해서 원인 분석/수정/검증을 수행합니다.
|
|
- “테스트 통과”는 최소 기준입니다. 실제 재현 시나리오(로그인, 새로고침, 리다이렉트 등)까지 확인한 뒤에만 이슈를 종료합니다.
|
|
- 관련 변경이 발생하면 테스트 문서(`docs/test-plan/*`, `docs/trouble-shooting/*`)를 함께 업데이트합니다.
|