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
22 changes: 22 additions & 0 deletions CHANGELOG.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ English: [CHANGELOG.md](CHANGELOG.md)

## [Unreleased]

## [0.4.0] — 2026-06-03

### Added
- **환경 간 설정 동기화 (ADR 0003).** 정의성 플랫폼 설정 — 권한, 역할(+ 권한 코드), 메뉴 —
을 대상 DB를 직접 손대지 않고, 이식 가능한 코드 기준 번들로 한 환경에서 다른 환경으로 승격.
- `GET /admin/api/v1/config/export`, `POST /admin/api/v1/config/import`.
- **merge**(기본, 추가형 — 생성·수정만, 삭제 없음) 와 **mirror**(대상을 번들과 일치: 역할
권한 재조정 + 번들에 없는 정의성 엔터티 삭제 — 메뉴 leaf-first; 사용자에게 할당된 역할은
skip; 권한은 revoke 후 삭제).
- **기본 dry-run** — import 는 섹션별 diff(생성 / 수정 / 삭제 / 건너뜀)를 반환하고
`dryRun=false` 가 아니면 아무것도 기록하지 않음.
- **옵트인 사용자 동기화**(`includeUsers`, 기본 off): export 는 사용자를 login id 기준으로
**비밀번호 없이** 내보내고, import 는 생성 전용이라 기존 사용자를 절대 덮어쓰지 않음.
- **기본 off**(`devslab.kit.config-sync.enabled`) + **운영 프로파일**(`prod`/`production`)
에서 **기동 거부**(ADR 0003 §5) — 설정은 배포 시 커밋된 번들로 승격하지, 운영에 즉석
push 하지 않음.
- [admin 콘솔](https://github.com/devslab-kr/devslab-kit-admin-ui)의 **Config Sync** 페이지가
export / import / dry-run diff / 적용을 담당.

### Notes
- 마이그레이션 불필요: 설정 동기화는 테이블을 추가하지 않으며, 명시적으로 켜기 전까지 비활성.

## [0.3.0] — 2026-06-02

### 변경됨 (Changed)
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ The library major aligns with the Spring Boot major: `4.x.y` targets Spring Boot

## [Unreleased]

## [0.4.0] — 2026-06-03

### Added
- **Config sync across environments (ADR 0003).** Promote definitional platform config —
permissions, roles (+ their permission codes) and menus — from one environment to another as
a portable, code-keyed bundle, instead of hand-editing each target database.
- `GET /admin/api/v1/config/export` and `POST /admin/api/v1/config/import`.
- **merge** (default, additive — creates and updates, never deletes) and **mirror** (makes the
target match the bundle: reconciles each role's grants and deletes definitional entities
absent from the bundle — menus leaf-first; a role still assigned to a user is skipped;
permissions revoked then deleted).
- **dry-run by default** — the import returns a per-section diff (created / updated / deleted /
skipped) and writes nothing unless `dryRun=false`.
- **Opt-in user sync** (`includeUsers`, default off): export carries users by login id with
**no password**; import is create-only and never overwrites an existing user.
- **Off by default** (`devslab.kit.config-sync.enabled`) and **refused under a production
profile** (`prod`/`production`, ADR 0003 §5) — promote config via the committed bundle on
deploy, not an ad-hoc push to prod.
- A **Config Sync** page in the [admin console](https://github.com/devslab-kr/devslab-kit-admin-ui)
drives export / import / dry-run diff / apply.

### Notes
- No migration required: config sync adds no tables and is inert unless explicitly enabled.

## [0.3.0] — 2026-06-02

### Changed
Expand Down
5 changes: 3 additions & 2 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
| **캐시** | 플러그형 캐시 — `in-memory` · `redis` · `none`. Redis 백엔드가 JSON 직렬화를 직접 책임지므로 `Serializable` 구현이나 직렬화기 배선이 필요 없습니다(ADR 0002). 사용자별 메뉴 캐시도 이 공유 매니저를 사용합니다. |
| **최초 관리자 부트스트랩** | 첫 부팅 시 테넌트, `PLATFORM_ADMIN` 역할, `admin.*` 권한, 관리자 사용자를 멱등하게 생성 — opt-in, 프로퍼티 기반(ADR 0001). |
| **관리자 REST API** | 위 모든 엔티티 + 진단 + 실시간 설정 뷰를 위한 `/admin/api/v1/**`. |
| **설정 동기화** | 정의성 설정(권한·역할·메뉴) — 그리고 옵트인으로 사용자 — 을 환경 간에 코드 기준 export/import 번들로 승격: `merge` 또는 `mirror`, 먼저 dry-run. 기본 off, 운영 프로파일에선 기동 거부(ADR 0003). |
| **OpenAPI / Swagger UI** | 스타터에 포함 — `/swagger-ui`가 관리자 API 그룹과 함께 자동으로 뜸, 설정 불필요. `openapi.enabled=false`로 끄거나, springdoc 의존성을 `exclude`해 jar 자체를 제거. |
| **Override 친화적** | 모든 기본 빈이 `@ConditionalOnMissingBean` — 직접 선언하면 어느 조각이든 교체 가능. |
| **GraalVM Native** | 리플렉션 중심 설계를 피하고, 샘플 앱이 `nativeCompile`을 검증. |
Expand All @@ -62,7 +63,7 @@
**Gradle (Kotlin DSL)**

```kotlin
implementation("kr.devslab:devslab-kit-spring-boot-starter:0.3.0")
implementation("kr.devslab:devslab-kit-spring-boot-starter:0.4.0")
```

**Maven**
Expand All @@ -71,7 +72,7 @@ implementation("kr.devslab:devslab-kit-spring-boot-starter:0.3.0")
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>devslab-kit-spring-boot-starter</artifactId>
<version>0.3.0</version>
<version>0.4.0</version>
</dependency>
```

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ specific product's domain.
| **Cache** | A pluggable cache — `in-memory`, `redis`, or `none`. The Redis backend owns JSON serialization, so you never implement `Serializable` or wire a serializer (ADR 0002). The per-user menu cache rides this shared manager. |
| **First-admin bootstrap** | Idempotently provisions a tenant, a `PLATFORM_ADMIN` role, the `admin.*` permissions, and an admin user on first boot — opt-in and property-driven (ADR 0001). |
| **Admin REST API** | `/admin/api/v1/**` for every entity above, plus diagnostics and a live settings view. |
| **Config sync** | Promote definitional config (permissions, roles, menus) — and, opt-in, users — across environments as a code-keyed export/import bundle: `merge` or `mirror`, dry-run first. Off by default, refused under a production profile (ADR 0003). |
| **OpenAPI / Swagger UI** | Bundled in the starter — `/swagger-ui` comes up with the admin API grouped, no wiring. Toggle off with `openapi.enabled=false`, or `exclude` the springdoc dependency to drop the jar. |
| **Override-friendly** | Every default bean is `@ConditionalOnMissingBean` — replace any piece by declaring your own. |
| **GraalVM Native** | Reflection-heavy patterns are avoided; the sample app verifies `nativeCompile`. |
Expand All @@ -64,7 +65,7 @@ specific product's domain.
**Gradle (Kotlin DSL)**

```kotlin
implementation("kr.devslab:devslab-kit-spring-boot-starter:0.3.0")
implementation("kr.devslab:devslab-kit-spring-boot-starter:0.4.0")
```

**Maven**
Expand All @@ -73,7 +74,7 @@ implementation("kr.devslab:devslab-kit-spring-boot-starter:0.3.0")
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>devslab-kit-spring-boot-starter</artifactId>
<version>0.3.0</version>
<version>0.4.0</version>
</dependency>
```

Expand Down
12 changes: 6 additions & 6 deletions docs/getting-started/installation.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
=== "Gradle (Kotlin DSL)"

```kotlin
implementation("kr.devslab:devslab-kit-spring-boot-starter:0.3.0")
implementation("kr.devslab:devslab-kit-spring-boot-starter:0.4.0")
```

=== "Gradle (Groovy)"

```groovy
implementation 'kr.devslab:devslab-kit-spring-boot-starter:0.3.0'
implementation 'kr.devslab:devslab-kit-spring-boot-starter:0.4.0'
```

=== "Maven"
Expand All @@ -32,7 +32,7 @@
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>devslab-kit-spring-boot-starter</artifactId>
<version>0.3.0</version>
<version>0.4.0</version>
</dependency>
```

Expand All @@ -43,10 +43,10 @@
물러납니다(`@ConditionalOnMissingBean`).

```kotlin
implementation("kr.devslab:devslab-kit-access-core:0.3.0") // RBAC + 그룹 + ABAC
implementation("kr.devslab:devslab-kit-cache-core:0.3.0") // 플러그형 캐시
implementation("kr.devslab:devslab-kit-access-core:0.4.0") // RBAC + 그룹 + ABAC
implementation("kr.devslab:devslab-kit-cache-core:0.4.0") // 플러그형 캐시
// …또는 계약만:
implementation("kr.devslab:devslab-kit-access-api:0.3.0")
implementation("kr.devslab:devslab-kit-access-api:0.4.0")
```

동작하는 앱을 부팅하려면 [빠른 시작](quick-start.md)을 참고하세요.
Expand Down
12 changes: 6 additions & 6 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ whole platform; depend on individual modules only if you want à la carte.
=== "Gradle (Kotlin DSL)"

```kotlin
implementation("kr.devslab:devslab-kit-spring-boot-starter:0.3.0")
implementation("kr.devslab:devslab-kit-spring-boot-starter:0.4.0")
```

=== "Gradle (Groovy)"

```groovy
implementation 'kr.devslab:devslab-kit-spring-boot-starter:0.3.0'
implementation 'kr.devslab:devslab-kit-spring-boot-starter:0.4.0'
```

=== "Maven"
Expand All @@ -32,7 +32,7 @@ whole platform; depend on individual modules only if you want à la carte.
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>devslab-kit-spring-boot-starter</artifactId>
<version>0.3.0</version>
<version>0.4.0</version>
</dependency>
```

Expand All @@ -44,10 +44,10 @@ your own — the auto-configuration backs off (`@ConditionalOnMissingBean`) when
do.

```kotlin
implementation("kr.devslab:devslab-kit-access-core:0.3.0") // RBAC + groups + ABAC
implementation("kr.devslab:devslab-kit-cache-core:0.3.0") // pluggable cache
implementation("kr.devslab:devslab-kit-access-core:0.4.0") // RBAC + groups + ABAC
implementation("kr.devslab:devslab-kit-cache-core:0.4.0") // pluggable cache
// …or just the contract:
implementation("kr.devslab:devslab-kit-access-api:0.3.0")
implementation("kr.devslab:devslab-kit-access-api:0.4.0")
```

See [Quick Start](quick-start.md) to boot a working app.
Expand Down
95 changes: 95 additions & 0 deletions docs/guides/config-sync.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# 설정 동기화 (Config Sync)

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

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

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

## 활성화

```yaml
devslab:
kit:
config-sync:
enabled: true # 기본 false — 끄면 엔드포인트·UI 전체가 비활성
```

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

## 번들에 담기는 것

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

모든 것이 DB UUID 가 아니라 **자연 코드**로 키잉되므로, id 가 다른 다른 환경에도 그대로
적용됩니다.

## 엔드포인트

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

## 모드

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

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

## 먼저 dry-run

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

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

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

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

`includeUsers=true` 시:

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

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

## 권장 워크플로

1. 로컬 DB 에서 설정 설계(admin 콘솔 또는 API).
2. 번들 **export**(정의성만이면 `includeUsers=false`).
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` 전환, 사용자 동기화 토글.
99 changes: 99 additions & 0 deletions docs/guides/config-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Config Sync

Promote **definitional platform config** — permissions, roles (and the permission codes
they grant) and menus — from one environment to another as a portable, code-keyed bundle,
instead of hand-editing each target database.

The admin console can manage the whole RBAC graph at runtime, but that config lives in the
**database**, not in code. A team designs it locally, then needs the *same* structure in
dev / staging / production — each with its own database. Config sync is the first-class way
to move it. See [ADR 0003](../adr/0003-config-sync.md) for the rationale and design.

!!! warning "Off by default, and never in production"
Config sync is a **dev/staging convenience**, disabled unless you opt in, and it
**refuses to start** under a `prod` / `production` profile. Production config is promoted
by applying the git-committed bundle on deploy — not by an ad-hoc push to a live system.

## Enable it

```yaml
devslab:
kit:
config-sync:
enabled: true # default false — the whole surface (endpoints + UI) is inert otherwise
```

If `enabled=true` while a `prod`/`production` profile is active, the application fails fast
at startup with a clear message rather than silently disabling the feature.

## What's in the bundle

| Included (definitional) | Excluded |
| --- | --- |
| Permissions (`code`, description) | Audit logs (history) |
| Roles (`code`, name, **permission codes**) | ABAC policies (these are *code*, not data) |
| Menus (`code`, parent code, label, path, icon, required permission code, order) | Passwords / secrets |
| Users — **opt-in only**, by login id, with role codes and **no password** | |

Everything is keyed by **natural codes**, never database UUIDs, so a bundle exported from one
environment applies cleanly to another whose ids differ.

## Endpoints

| | |
| --- | --- |
| `GET /admin/api/v1/config/export?tenantId={t}&includeUsers=false` | Returns the bundle as JSON. |
| `POST /admin/api/v1/config/import?mode=merge&dryRun=true&includeUsers=false` | Applies a bundle; returns a per-section diff. |

## Modes

- **`merge`** (default) — additive. Creates and updates; **never deletes**, and never revokes
a role's existing grants. Idempotent: re-applying the same bundle changes nothing.
- **`mirror`** — makes the target *match the bundle exactly*. On top of the merge it reconciles
each role's grants (revoking permissions the bundle omits) and **deletes** definitional
entities absent from the bundle:
- **menus** are deleted leaf-first (a child before its parent);
- a **role still assigned to a user is skipped** — mirror never strips a user's role;
- **permissions** are revoked from the tenant's roles, then deleted.

!!! danger "Mirror deletes"
`mirror` removes things. Always review the **dry-run** diff before applying, and prefer it
only for single-tenant-per-deployment setups (permissions are global).

## Dry-run first

`dryRun=true` is the **default**. The import computes the full diff and writes nothing. The
result reports, per section (`permissions` / `roles` / `menus` / `users`), what would be:

- **created**, **updated**, **deleted** (mirror only), and **skipped** (a role in use, or an
existing user).

Apply for real with `dryRun=false` once the preview matches your intent.

## User sync (opt-in)

With `includeUsers=true`:

- **export** carries users by login id — email, status and role codes — but **never a
password**.
- **import** is **create-only**: a missing user is created with no usable password and
`mustChangePassword`, then assigned its roles by code. An **existing user is never
overwritten** (it is reported as `skipped`). Set the password via the admin console afterwards.

Users are operational data; leave `includeUsers` off unless you specifically want to seed
accounts into a fresh environment.

## Recommended workflow

1. Design config locally against your local database (admin console or API).
2. **Export** the bundle (`includeUsers=false` for definitional-only).
3. Commit the bundle JSON to git — it is now reviewable, versioned config.
4. On the target, **dry-run** the import and review the diff.
5. Apply (`dryRun=false`). Use `merge` to add/update; `mirror` only when you intend the target
to match the bundle exactly.

## Admin console

The [admin console](https://github.com/devslab-kr/devslab-kit-admin-ui) has a **Config Sync**
page that drives the whole flow: export (view / download / copy), import (paste or upload →
dry-run diff → apply), the `merge`/`mirror` switch, and the user-sync toggle.
5 changes: 5 additions & 0 deletions docs/index.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
`in-memory`, `redis`, `none`. Redis 백엔드가 JSON 직렬화를 직접 책임집니다 —
`Serializable`도, 직렬화기 배선도 필요 없습니다.

- :material-sync: **설정 동기화**

권한·역할·메뉴를 환경 간에 코드 기준 export/import 번들로 승격 — `merge` 또는
`mirror`, 먼저 dry-run ([가이드](guides/config-sync.ko.md)).

</div>

## 왜 스타터인가? { #why-a-starter }
Expand Down
Loading
Loading