Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/getting-started/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ devslab:
In production: set a real `identity.jwt.secret`, and for the bootstrap either
set a strong `admin-password` (with `must-change-password: true`) or leave it
blank to have a random one generated and logged once. See
[Configuration](../reference/configuration.md#first-admin-bootstrap-devslabkitbootstrap).
[Configuration](../reference/configuration.md#bootstrap).

---

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/admin-console.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ kit의 **적용된 설정**(`devslab.kit.*`)을 읽기 전용으로 보는 화
감사, 캐시. 시크릿은 마스킹. 실행 중인 앱이 실제로 무엇을 로드했는지 확인용. (값 변경은
`application.yml` 에서 — [설정](../reference/configuration.md) 참고.)

### Config Sync (설정 동기화)
### Config Sync (설정 동기화) { #config-sync }
정의성 설정(권한·역할·메뉴)을 환경 간 승격. **기본 off**, 운영 프로파일에선 거부됨([Config Sync
가이드](config-sync.md) 참고).

Expand Down
81 changes: 59 additions & 22 deletions docs/guides/bootstrap.ko.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,91 @@
# 최초 관리자 부트스트랩

빈 데이터베이스에는 사용자가 없습니다 — 그럼 영구 백도어 없이 처음에 어떻게 로그인할까요?
kit의 **최초 관리자 부트스트랩**이 첫 부팅 시 사용 가능한 관리자를 프로비저닝합니다.
opt-in이며 프로퍼티 기반입니다(배경: [ADR 0001](../adr/0001-bootstrap-admin.md)).
kit의 **최초 관리자 부트스트랩**이 첫 부팅 때 쓸 수 있는 관리자를 만들어 줍니다: opt-in,
속성 기반, 멱등(idempotent)입니다(배경: [ADR 0001](../adr/0001-bootstrap-admin.md)).

## 하는 일
처음이면 [튜토리얼](../getting-started/tutorial.md)이 바로 이걸로 로그인까지 데려갑니다 — 이
가이드는 그 단계의 레퍼런스입니다.

`bootstrap.enabled = true`이면 시작 시 kit이 (없을 때만) **멱등하게** 생성합니다:
## 무엇을 하나

`bootstrap.enabled = true`면, 시작 시 kit이 **멱등하게** (없을 때만) 생성합니다:

1. 테넌트 `bootstrap.tenant-id`,
2. 전체 `admin.*` 권한 세트를 가진 `PLATFORM_ADMIN` 역할,
3. 그 테넌트에 그 역할을 가진 관리자 사용자(`bootstrap.admin-login-id`).
2. 전체 `admin.*` 권한을 가진 `PLATFORM_ADMIN` 역할,
3. 그 역할을 가진 관리자 사용자(`bootstrap.admin-login-id`)를 해당 테넌트에.

멱등이므로 켜둔 채로 둬도 안전합니다 — 이후 부팅은 레코드를 찾고 아무것도 하지 않습니다.
멱등이므로 둬도 안전합니다 — 이후 부팅은 레코드를 찾고 아무것도 하지 않습니다.

## 설정

```yaml
# src/main/resources/application.yml
devslab:
kit:
bootstrap:
enabled: true
tenant-id: default
admin-login-id: admin
admin-password: ${DEVSLAB_BOOTSTRAP_ADMIN_PASSWORD:} # 비우면 랜덤, 한 번 로깅
admin-password: ${DEVSLAB_BOOTSTRAP_ADMIN_PASSWORD:} # 비우면 랜덤, 한 번만 로그
must-change-password: true
```

모든 키는 [설정 레퍼런스](../reference/configuration.md)를 참고하세요.
모든 키는 [설정 레퍼런스](../reference/configuration.md#bootstrap) 참고.

## 비밀번호

- 알려진 값이 필요하면 **명시적으로 설정**(예: 로컬 개발 `admin`/`admin`).
- **비우면** kit이 강력한 랜덤 비밀번호를 생성해 시작 시 **한 ** 로깅합니다
로그에서 복사하면 이후엔 사라집니다.
- 알려진 값이 필요하면 **명시 설정**(예: 로컬 개발 `admin`/`admin`).
- **비워 두면** kit이 강한 랜덤 비밀번호를 생성해 시작 시 **한 번만** 로그에 찍습니다
로그에서 복사하면 그 뒤엔 사라집니다.
- `prod` / `production` 프로파일에서는 약한 부트스트랩 비밀번호로 **시작을 거부**하므로,
placeholder가 운영에 새지 않습니다.
플레이스홀더가 운영에 새어 들어가지 않습니다.

알려진 비밀번호는 `must-change-password: true`와 함께 써서 운영자가 첫 로그인 때 교체하게
하세요.

## 처음 로그인하기

위 설정(`admin`/`admin`)으로 앱을 시작한 뒤:

=== "관리자 콘솔"

1. [관리자 콘솔](admin-console.md)을 띄우고 브라우저에서 엽니다.
2. `admin` / `admin`(테넌트 `default`)으로 로그인.
3. 이제 `PLATFORM_ADMIN`을 가졌으니 모든 화면을 쓸 수 있습니다.

알려진 비밀번호는 `must-change-password: true`와 함께 써서 운영자가 첫 로그인 시
교체하게 하세요.
=== "REST API"

## 최초 실행 감지
```bash
# 자격 증명을 JWT로 교환:
curl -X POST localhost:8080/admin/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"tenantId":"default","loginId":"admin","rawPassword":"admin"}'
# → { "token": "eyJ…" } — `Authorization: Bearer eyJ…`로 전송
```

비인증 엔드포인트 `GET /admin/api/v1/bootstrap/status`가 `{ "initialized": boolean }`을
반환합니다. 설정 마법사나 랜딩 페이지가 이를 기준으로 분기할 수 있습니다 — 예: 갓 배포된
환경을 로그인 폼 대신 "관리자 생성" 플로우로 보냄.
## 첫 실행 감지

인증 없이 호출하는 `GET /admin/api/v1/bootstrap/status`는 `{ "initialized": boolean }`을
반환합니다. 셋업 마법사나 랜딩 페이지가 이걸로 분기할 수 있습니다 — 예: 갓 배포된 인스턴스를
로그인 폼 대신 "관리자 만들기" 흐름으로 보냅니다:

```bash
curl localhost:8080/admin/api/v1/bootstrap/status
# → { "initialized": false } (아직 관리자 없음) / true (프로비저닝됨)
```

일부러 공개입니다 — 마법사는 누가 인증하기 *전에* 이걸 호출합니다.

## 운영 가이드

실제 환경에서는 다음 중 하나를 권장합니다:
실제 환경에서는 둘 중 하나를 권장합니다:

- 강한 `admin-password`(시크릿으로 주입) + `must-change-password: true`, 또는
- `enabled: false`로 두고 최초 관리자를 외부에서 프로비저닝(SQL/마이그레이션/운영 도구).

## 더 보기

- 강력한 `admin-password`(시크릿으로 주입) + `must-change-password: true`, 또는
- `enabled: false`로 두고 최초 관리자를 out-of-band(SQL/마이그레이션/운영 도구)로 프로비저닝.
- [튜토리얼](../getting-started/tutorial.md) — 실행 중인 앱에서의 부트스트랩.
- [Access (RBAC + ABAC)](access.md) — `PLATFORM_ADMIN`과 `admin.*`이 부여하는 것.
- [설정 레퍼런스](../reference/configuration.md#bootstrap) — 모든 키.
44 changes: 41 additions & 3 deletions docs/guides/bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

A fresh database has no users — so how do you log in the first time without a
permanent backdoor? The kit's **first-admin bootstrap** provisions a usable admin
on first boot, opt-in and property-driven (background: [ADR 0001](../adr/0001-bootstrap-admin.md)).
on first boot: opt-in, property-driven, and idempotent (background:
[ADR 0001](../adr/0001-bootstrap-admin.md)).

New here? This is exactly what the [Tutorial](../getting-started/tutorial.md) uses to
get you logged in — this guide is the reference behind that step.

## What it does

Expand All @@ -19,6 +23,7 @@ nothing.
## Configuration

```yaml
# src/main/resources/application.yml
devslab:
kit:
bootstrap:
Expand All @@ -29,7 +34,7 @@ devslab:
must-change-password: true
```

See the [Configuration reference](../reference/configuration.md) for every key.
See the [Configuration reference](../reference/configuration.md#bootstrap) for every key.

## The password

Expand All @@ -42,16 +47,49 @@ See the [Configuration reference](../reference/configuration.md) for every key.
Pair a known password with `must-change-password: true` so the operator rotates
it on first login.

## Log in for the first time

With the config above (`admin`/`admin`), start the app, then:

=== "Admin console"

1. Start the [admin console](admin-console.md) and open it in the browser.
2. Log in with `admin` / `admin` (tenant `default`).
3. You now hold `PLATFORM_ADMIN` — every screen is available.

=== "REST API"

```bash
# exchange credentials for a JWT:
curl -X POST localhost:8080/admin/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"tenantId":"default","loginId":"admin","rawPassword":"admin"}'
# → { "token": "eyJ…" } — send it as `Authorization: Bearer eyJ…`
```

## First-run detection

The unauthenticated endpoint `GET /admin/api/v1/bootstrap/status` returns
`{ "initialized": boolean }`. A setup wizard or landing page can branch on it —
e.g. send a brand-new deployment to a "create your admin" flow instead of a login
form.
form:

```bash
curl localhost:8080/admin/api/v1/bootstrap/status
# → { "initialized": false } (no admin yet) / true (provisioned)
```

It's public on purpose — the wizard calls it *before* anyone can authenticate.

## Production guidance

For real environments, prefer one of:

- a strong `admin-password` (injected as a secret) + `must-change-password: true`, or
- `enabled: false` and provision the first admin out-of-band (SQL/migration/ops tooling).

## See also

- [Tutorial](../getting-started/tutorial.md) — bootstrap in a running app.
- [Access (RBAC + ABAC)](access.md) — what `PLATFORM_ADMIN` and `admin.*` grant.
- [Configuration reference](../reference/configuration.md#bootstrap) — every key.
132 changes: 81 additions & 51 deletions docs/guides/config-sync.ko.md
Original file line number Diff line number Diff line change
@@ -1,95 +1,125 @@
# 설정 동기화 (Config Sync)

**정의성 플랫폼 설정** — 권한, 역할(과 그 역할이 부여하는 권한 코드), 메뉴 — 을 대상 DB를
직접 손대지 않고, 이식 가능한 코드 기준 번들로 한 환경에서 다른 환경으로 승격합니다.
일일이 손으로 고치는 대신, 휴대 가능한 **코드 기반 번들**로 한 환경에서 다른 환경으로
승격합니다.

admin 콘솔은 RBAC 그래프 전체를 런타임에 관리할 수 있지만, 그 설정은 코드가 아니라
**데이터베이스**에 있습니다. 팀은 로컬에서 설계한 뒤 dev / staging / 운영 — 각자 자기 DB —
*같은* 구조가 필요해집니다. 설정 동기화가 이를 옮기는 일급 수단입니다. 배경과 설계는
[ADR 0003](../adr/0003-config-sync.md) 참고.
관리자 콘솔은 RBAC 그래프 전체를 런타임에 관리할 수 있지만, 그 설정은 코드가 아니라
**데이터베이스**에 있습니다. 팀이 로컬에서 설계한 뒤, 각자 DB를 가진 dev / staging /
production에 *같은* 구조가 필요해집니다. 설정 동기화가 이를 옮기는 일급 수단입니다. 근거와
설계는 [ADR 0003](../adr/0003-config-sync.md) 참고.

!!! warning "기본 off, 운영에선 금지"
설정 동기화는 **dev/staging 편의 도구**로, 명시적으로 켜기 전까지 비활성이며 `prod` /
`production` 프로파일에서는 **기동을 거부**합니다. 운영 설정은 배포 시 git 에 커밋된 번들을
적용해 승격하지, 라이브 시스템에 즉석으로 push 하지 않습니다.
!!! warning "기본 off, 운영에서는 절대 안 됨"
설정 동기화는 **dev/staging 편의 기능**으로, opt-in 하지 않으면 비활성이고, `prod` /
`production` 프로파일에서는 **시작을 거부**합니다. 운영 설정은 라이브 시스템에 즉석으로
푸시하는 게 아니라, git에 커밋된 번들을 배포 시 적용해서 승격합니다.

## 활성화

```yaml
# src/main/resources/application.yml
devslab:
kit:
config-sync:
enabled: true # 기본 false — 끄면 엔드포인트·UI 전체가 비활성
enabled: true # 기본 false — 아니면 엔드포인트+UI 전체가 비활성
```

`enabled=true` 인데 `prod`/`production` 프로파일이 활성이면, 기능을 조용히 끄는 대신 명확한
메시지와 함께 기동 단계에서 즉시 실패합니다.
`prod`/`production` 프로파일이 활성인데 `enabled=true`면, 조용히 비활성화하지 않고 시작 시
명확한 메시지와 함께 fail-fast 합니다.

## 번들에 담기는 것

| 포함(정의성) | 제외 |
| --- | --- |
| 권한 (`code`, 설명) | 감사 로그 (이력) |
| 역할 (`code`, 이름, **권한 코드**) | ABAC 정책 (이건 데이터가 아니라 *코드*) |
| 메뉴 (`code`, 부모 코드, 라벨, 경로, 아이콘, 필요 권한 코드, 순서) | 비밀번호 / 시크릿 |
| 사용자 — **옵트인 시에만**, login id 기준, 역할 코드 + **비밀번호 없음** | |
| 권한(`code`, 설명) | 감사 로그(이력) |
| 역할(`code`, 이름, **권한 코드들**) | ABAC 정책(이건 *코드*지 데이터가 아님) |
| 메뉴(`code`, 부모 코드, 라벨, 경로, 아이콘, 필요 권한 코드, 순서) | 비밀번호 / 시크릿 |
| 사용자 — **opt-in 시에만**, login id 기준, 역할 코드 포함, **비밀번호 없음** | |

모든 것이 DB UUID 가 아니라 **자연 코드**로 키잉되므로, id 가 다른 다른 환경에도 그대로
적용됩니다.
모든 것은 DB UUID가 아니라 **자연 코드**로 키잉되므로, 한 환경에서 export한 번들이 id가 다른
다른 환경에도 깔끔하게 적용됩니다.

## 엔드포인트

| | |
| --- | --- |
| `GET /admin/api/v1/config/export?tenantId={t}&includeUsers=false` | 번들을 JSON 으로 반환. |
| `GET /admin/api/v1/config/export?tenantId={t}&includeUsers=false` | 번들을 JSON으로 반환. |
| `POST /admin/api/v1/config/import?mode=merge&dryRun=true&includeUsers=false` | 번들 적용; 섹션별 diff 반환. |

## 전체 왕복(round trip)

로컬에서 staging으로 설정 옮기기 — 복붙:

```bash
# 1. 로컬 — 정의성 번들을 파일로 export
curl -G localhost:8080/admin/api/v1/config/export \
-H 'Authorization: Bearer <local-token>' \
--data-urlencode 'tenantId=default' \
--data-urlencode 'includeUsers=false' \
-o config-bundle.json

# 2. 커밋해서 승격을 리뷰 가능·버전 관리되게
git add config-bundle.json && git commit -m "chore: rbac bundle"

# 3. staging — 먼저 DRY-RUN(아무것도 안 쓰고 diff 반환)
curl -X POST 'https://staging.example.com/admin/api/v1/config/import?mode=merge&dryRun=true' \
-H 'Authorization: Bearer <staging-token>' \
-H 'Content-Type: application/json' \
--data-binary @config-bundle.json

# 4. diff 검토 후 실제 적용
curl -X POST 'https://staging.example.com/admin/api/v1/config/import?mode=merge&dryRun=false' \
-H 'Authorization: Bearer <staging-token>' \
-H 'Content-Type: application/json' \
--data-binary @config-bundle.json
```

## 모드

- **`merge`**(기본) — 추가형. 생성·수정만 하고 **삭제하지 않으며**, 역할의 기존 권한도 회수하지
않음. 멱등: 같은 번들을 다시 적용해도 변화 없음.
- **`mirror`** — 대상을 *번들과 정확히 일치*시킴. merge 에 더해 역할 권한을 재조정(번들에 없는
권한 회수)하고, 번들에 없는 정의성 엔터티를 **삭제**:
- **메뉴**는 leaf-first(자식 먼저, 부모 나중)로 삭제;
- **사용자에게 할당된 역할은 skip** — mirror 가 사용자 역할을 함부로 떼지 않음;
- **권한**은 테넌트 역할에서 회수 후 삭제.
- **`merge`**(기본) — 추가형. 생성·갱신만 하고 **삭제하지 않으며**, 역할의 기존 부여도 회수하지
않습니다. 멱등: 같은 번들을 다시 적용해도 아무것도 안 바뀜.
- **`mirror`** — 대상을 *번들과 정확히 일치*시킵니다. merge에 더해 각 역할의 부여를 조정하고
(번들에 없는 권한 회수), 번들에 없는 정의성 엔터티를 **삭제**합니다:
- **메뉴**는 leaf부터 삭제(자식 먼저, 부모 나중);
- **사용자에게 아직 배정된 역할은 건너뜀** — mirror는 사용자의 역할을 벗기지 않음;
- **권한**은 테넌트의 역할들에서 회수한 뒤 삭제.

!!! danger "미러는 삭제합니다"
`mirror` 는 항목을 제거합니다. 적용 전 항상 **dry-run** diff 를 확인하고, 단일 테넌트 배포
환경에서만 권장합니다(권한은 전역).
!!! danger "Mirror는 삭제한다"
`mirror`는 무언가를 제거합니다. 적용 전 항상 **dry-run** diff를 검토하고, 배포당 단일
테넌트 구성에서만 쓰세요(권한은 전역입니다).

## 먼저 dry-run
## Dry-run 먼저

`dryRun=true` 가 **기본**입니다. import 는 전체 diff 를 계산하고 아무것도 기록하지 않습니다.
결과는 섹션별(`permissions` / `roles` / `menus` / `users`)로 다음을 보고합니다:
`dryRun=true`가 **기본**입니다. import는 전체 diff를 계산하고 아무것도 쓰지 않습니다. 결과는
섹션별(`permissions` / `roles` / `menus` / `users`)로 무엇이 다음이 될지 보고합니다:

- **생성(created)**, **수정(updated)**, **삭제(deleted, 미러 전용)**, **건너뜀(skipped — 사용
중인 역할, 또는 이미 존재하는 사용자)**.
- **created**, **updated**, **deleted**(mirror만), **skipped**(사용 중인 역할, 또는 기존
사용자).

미리보기가 의도와 맞으면 `dryRun=false` 로 실제 적용합니다.
미리보기가 의도와 맞으면 `dryRun=false`로 실제 적용.

## 사용자 동기화 (옵트인)
## 사용자 동기화 (opt-in)

`includeUsers=true`:
`includeUsers=true`:

- **export** 는 사용자를 login id 기준으로 — 이메일·상태·역할 코드 — 내보내되 **비밀번호는
절대 포함하지 않음**.
- **import** 는 **생성 전용**: 없는 사용자만 사용 불가 비밀번호 + `mustChangePassword` 로 생성한
코드로 역할을 할당. **기존 사용자는 절대 덮어쓰지 않음**(`skipped` 로 보고). 비밀번호는
이후 admin 콘솔에서 설정.
- **export**는 사용자를 login id로 담습니다 — 이메일, 상태, 역할 코드 — 하지만 **비밀번호는
절대** 안 담음.
- **import**는 **생성 전용**: 없는 사용자는 사용 불가 비밀번호 + `mustChangePassword`
생성된 뒤 역할이 코드로 배정됩니다. **기존 사용자는 절대 덮어쓰지 않음**(`skipped`로 보고).
비밀번호는 이후 관리자 콘솔에서 설정.

사용자는 운영 데이터입니다. 새 환경에 계정을 시드할 의도가 아니면 `includeUsers` 는 꺼 두세요.
사용자는 운영 데이터입니다; 새 환경에 계정을 심을 의도가 아니면 `includeUsers`는 꺼 두세요.

## 권장 워크플로

1. 로컬 DB 에서 설정 설계(admin 콘솔 또는 API).
1. 로컬 DB에서 설정 설계(관리자 콘솔 또는 API).
2. 번들 **export**(정의성만이면 `includeUsers=false`).
3. 번들 JSON 을 git 에 커밋 — 이제 리뷰·버전 관리되는 설정.
4. 대상에서 import **dry-run** 후 diff 확인.
5. 적용(`dryRun=false`). 추가/수정은 `merge`, 대상을 번들과 정확히 일치시킬 때만 `mirror`.
3. 번들 JSON을 git에 커밋 — 이제 리뷰 가능·버전 관리되는 설정.
4. 대상에서 import **dry-run** 후 diff 검토.
5. 적용(`dryRun=false`). 추가/갱신은 `merge`; 대상을 번들과 정확히 일치시킬 의도일 때만 `mirror`.

## Admin 콘솔
## 관리자 콘솔

[admin 콘솔](https://github.com/devslab-kr/devslab-kit-admin-ui)에 전체 흐름을 다루는 **Config
Sync** 페이지가 있습니다: export(보기 / 다운로드 / 복사), import(붙여넣기·업로드 → dry-run
diff → 적용), `merge`/`mirror` 전환, 사용자 동기화 토글.
[관리자 콘솔](admin-console.md#config-sync)에는 전체 흐름을 다루는 **Config Sync** 페이지가
있습니다: export(보기 / 다운로드 / 복사), import(붙여넣기 또는 업로드 → dry-run diff → 적용),
`merge`/`mirror` 스위치, 사용자 동기화 토글.
Loading
Loading