diff --git a/docs/getting-started/quick-start.ko.md b/docs/getting-started/quick-start.ko.md index 6ed2028..a6ed57d 100644 --- a/docs/getting-started/quick-start.ko.md +++ b/docs/getting-started/quick-start.ko.md @@ -59,7 +59,7 @@ devslab: ```bash curl -s localhost:8080/admin/api/v1/auth/login \ -H 'Content-Type: application/json' \ - -d '{"tenantId":"default","loginId":"admin","password":"<부트스트랩 비밀번호>"}' + -d '{"tenantId":"default","loginId":"admin","rawPassword":"<부트스트랩 비밀번호>"}' ``` `bootstrap.admin-password`를 비우면 kit이 강력한 랜덤 비밀번호를 만들어 시작 시 한 번 diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index d85575c..9ca806b 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -59,7 +59,7 @@ Call the admin API directly, or point the ```bash curl -s localhost:8080/admin/api/v1/auth/login \ -H 'Content-Type: application/json' \ - -d '{"tenantId":"default","loginId":"admin","password":""}' + -d '{"tenantId":"default","loginId":"admin","rawPassword":""}' ``` A blank `bootstrap.admin-password` makes the kit generate a strong random one and diff --git a/docs/getting-started/tutorial.ko.md b/docs/getting-started/tutorial.ko.md new file mode 100644 index 0000000..74ed54c --- /dev/null +++ b/docs/getting-started/tutorial.ko.md @@ -0,0 +1,341 @@ +# 튜토리얼: 0에서 실행까지 + +**devslab-kit을 한 번도 안 써본 사람**을 위한, 복붙으로 따라 하는 완전 가이드입니다. 끝까지 +하면 로그인, 관리자 계정, 역할·권한, 직접 만든 권한 보호 엔드포인트, 테넌트 단위 데이터, ABAC +정책까지 **로컬에서 동작**합니다. + +사전 지식 없다고 가정합니다. 모든 명령과 파일을 전부 보여줍니다. + +!!! info "먼저 필요한 것" + - **JDK 21** (`java -version` 이 21을 출력) + - **Docker** (PostgreSQL 실행용 — `docker info` 가 성공해야 함) + - 터미널. IDE(IntelliJ / VS Code)는 있으면 좋지만 필수는 아님. + +--- + +## 1단계 — Spring Boot 프로젝트 만들기 + +최소 Spring Boot 4 프로젝트를 생성합니다([start.spring.io](https://start.spring.io)에서 Gradle + +Java 21 + Spring Boot 4.x). 또는 빈 폴더 `myapp/` 에 아래 두 파일만 만들어도 됩니다. + +**`settings.gradle.kts`** + +```kotlin +rootProject.name = "myapp" +``` + +**`build.gradle.kts`** + +```kotlin +plugins { + java + id("org.springframework.boot") version "4.0.6" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "com.example" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { languageVersion = JavaLanguageVersion.of(21) } +} + +repositories { mavenCentral() } + +dependencies { + // 플랫폼: 인증, RBAC + 그룹 + ABAC, 멀티테넌시, 동적 메뉴, 감사 로깅, + // 관리자 REST API — 전부 자동 구성. + implementation("kr.devslab:devslab-kit-spring-boot-starter:0.4.2") + + // devslab-kit은 어떤 Spring 스타터를 쓸지 강요하지 않습니다. + // 이 튜토리얼에선 web + security + JPA + Flyway + PostgreSQL. + implementation("org.springframework.boot:spring-boot-starter-webmvc") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.springframework.boot:spring-boot-starter-flyway") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.flywaydb:flyway-database-postgresql") + runtimeOnly("org.postgresql:postgresql") + + // `bootRun` 시 compose.yaml 의 Postgres 컨테이너를 자동으로 띄워줍니다. + developmentOnly("org.springframework.boot:spring-boot-docker-compose") +} +``` + +Gradle 래퍼가 없으면 추가하세요: `gradle wrapper` (또는 다른 Spring 프로젝트의 `gradlew` 복사). + +메인 클래스도 필요합니다 — **`src/main/java/com/example/myapp/MyappApplication.java`**: + +```java +package com.example.myapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MyappApplication { + public static void main(String[] args) { + SpringApplication.run(MyappApplication.class, args); + } +} +``` + +--- + +## 2단계 — Docker로 PostgreSQL 띄우기 + +kit은 모든 것을 PostgreSQL에 저장합니다. 프로젝트 루트에 **`compose.yaml`** 생성: + +```yaml +services: + postgres: + image: 'postgres:16-alpine' + environment: + - 'POSTGRES_DB=myapp' + - 'POSTGRES_USER=myapp' + - 'POSTGRES_PASSWORD=myapp' + ports: + - '5432:5432' +``` + +`spring-boot-docker-compose` 를 넣었으니 **앱 실행 시 Spring Boot가 이 컨테이너를 자동으로 +시작**합니다 — 직접 `docker compose up` 안 해도 됩니다. + +--- + +## 3단계 — 앱 설정 + +**`src/main/resources/application.yml`** 생성: + +```yaml +spring: + datasource: + url: jdbc:postgresql://localhost:5432/myapp + username: myapp + password: myapp + jpa: + open-in-view: false + hibernate: + ddl-auto: validate # 스키마는 kit이 Flyway로 관리 — Hibernate가 건드리지 않게 + +devslab: + kit: + tenant: + mode: single # 앱 전체가 단일 테넌트 + resolver: fixed + default-tenant-id: default + identity: + jwt: + secret: dev-only-change-me-32-bytes-minimum! # 32바이트 이상; 운영에선 시크릿 사용 + ttl: PT8H # 액세스 토큰 수명 (ISO-8601 duration) + cache: + type: in-memory # in-memory | redis | none + bootstrap: + enabled: true # 첫 부팅 시 최초 관리자 생성 + admin-login-id: admin + admin-password: admin # 개발 전용 — 아래 경고 참고 + must-change-password: false +``` + +!!! warning "이 값들은 개발 전용입니다" + 운영에선: 실제 `identity.jwt.secret` 설정, bootstrap은 강한 `admin-password`(+ + `must-change-password: true`)를 쓰거나 비워서 랜덤 생성·1회 로깅되게 하세요. + [설정 레퍼런스](../reference/configuration.md) 참고. + +--- + +## 4단계 — 실행 + +```bash +./gradlew bootRun +``` + +첫 실행 시 kit은: + +1. (Docker Compose로) Postgres 컨테이너를 시작하고, +2. **Flyway**로 `platform_*` 테이블 생성(전용 history 테이블을 써서, 나중에 당신이 `db/migration` + 에 추가할 마이그레이션과 충돌하지 않음), +3. 테넌트, 모든 `admin.*` 권한을 가진 `PLATFORM_ADMIN` 역할, `admin` 사용자를 **부트스트랩**, +4. `/admin/api/v1/**` 에 **관리자 REST API**, `/swagger-ui.html` 에 **Swagger UI** 제공. + +이걸 띄워둔 채로 두 번째 터미널을 열어 다음 단계를 진행하세요. + +--- + +## 5단계 — 로그인 + +모든 관리자 호출엔 토큰이 필요합니다. 부트스트랩 관리자로 로그인: + +```bash +curl -s localhost:8080/admin/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","loginId":"admin","rawPassword":"admin"}' +``` + +응답에 JWT가 들어있습니다. 다음 명령에서 재사용하도록 셸 변수에 담으세요: + +```bash +TOKEN=$(curl -s localhost:8080/admin/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","loginId":"admin","rawPassword":"admin"}' | sed -E 's/.*"token":"([^"]+)".*/\1/') +echo "$TOKEN" +``` + +!!! tip "UI가 편하시면" + [admin 콘솔](https://github.com/devslab-kr/devslab-kit-admin-ui)을 `http://localhost:8080` + 에 연결하면 6단계를 curl 대신 클릭으로 할 수 있습니다. + +--- + +## 6단계 — 권한·역할·사용자 만들기 + +**권한**은 문자열 코드(`resource.action`), **역할**은 권한 묶음, **사용자**는 역할을 가집니다. +새 사용자에게 "책 읽기" 권한을 줘봅시다. + +```bash +# 1) 권한 생성 -> 응답의 "id" 기록 +curl -s -X POST localhost:8080/admin/api/v1/permissions \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"code":"book.read","description":"책 읽기"}' + +# 2) 역할 생성 -> "id" 기록 +curl -s -X POST localhost:8080/admin/api/v1/roles \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","code":"LIBRARIAN","name":"사서"}' + +# 3) 역할에 권한 부여 (1·2의 id 사용) +curl -s -X POST "localhost:8080/admin/api/v1/roles//permissions/" \ + -H "Authorization: Bearer $TOKEN" + +# 4) 사용자 생성 +curl -s -X POST localhost:8080/admin/api/v1/users \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","loginId":"alice","rawPassword":"alice-password","email":"alice@example.com"}' + +# 5) 사용자에게 역할 할당 (2·4의 id 사용) +curl -s -X POST "localhost:8080/admin/api/v1/roles//users/?tenantId=default" \ + -H "Authorization: Bearer $TOKEN" +``` + +이제 `alice`로 로그인(5단계, alice 자격증명)하면 `book.read` 권한을 가집니다. + +--- + +## 7단계 — 내 엔드포인트 보호하기 + +kit은 `PermissionChecker` 빈을 제공합니다. 주입해서 코드를 게이트하세요: + +```java +package com.example.myapp; + +import kr.devslab.kit.access.Permission; +import kr.devslab.kit.access.PermissionChecker; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +class BookController { + + private final PermissionChecker access; + + BookController(PermissionChecker access) { + this.access = access; + } + + @GetMapping("/api/books") + String listBooks() { + // 권한이 없으면 PermissionDeniedException(-> 403) 발생. + access.check(Permission.of("book.read")); + return "here are the books"; + } +} +``` + +alice 토큰으로 호출 → `200`, `book.read` 없는 사용자 → `403`. +다른 메서드: `hasPermission(...)`, `hasAnyPermission(...)`, `hasAllPermissions(...)`. + +--- + +## 8단계 — 호출자 파악 + 테넌트 단위로 데이터 격리 + +자주 쓰게 될 두 빈: + +```java +import kr.devslab.kit.identity.CurrentUserProvider; +import kr.devslab.kit.tenant.TenantContextHolder; + +// 인증된 사용자는 누구인가? +String loginId = currentUserProvider.current() + .map(u -> u.loginId()) + .orElseThrow(); + +// 이 요청의 테넌트는? (항상 존재 — 단일 테넌트는 "default" 반환) +String tenantId = tenantContextHolder.current() + .map(ctx -> ctx.tenantId().value()) + .orElseThrow(); +``` + +당신의 엔터티에 `tenantId`를 저장하고 모든 쿼리를 그걸로 필터링하면 멀티테넌시 끝입니다. +`single`이든 `multi` 모드든 코드는 동일합니다([멀티테넌시 가이드](../guides/tenancy.md)). + +--- + +## 9단계 — 속성 기반(ABAC) 규칙 추가 + +RBAC는 "사용자가 `book.read`를 가졌나?"에 답하고, ABAC는 "…**이 특정 책에 대해, 지금?**"까지 +답합니다. **`Policy`** 빈을 작성하면 kit이 수집해 이름으로 디스패치합니다. + +```java +package com.example.myapp; + +import kr.devslab.kit.access.policy.Policy; +import kr.devslab.kit.access.policy.PolicyContext; +import kr.devslab.kit.access.policy.PolicyDecision; +import org.springframework.stereotype.Component; + +@Component +class BookOwnerPolicy implements Policy { + + @Override public String name() { return "book-owner"; } + + @Override + public PolicyDecision evaluate(PolicyContext ctx) { + // 예: 소유자만 그 책을 다룰 수 있음 + Object owner = ctx.resourceAttributes().get("ownerLoginId"); + boolean isOwner = ctx.userId().isPresent() && /* owner와 비교 */ owner != null; + return isOwner ? PolicyDecision.PERMIT : PolicyDecision.DENY; + } +} +``` + +ABAC 오버로드로 게이트하고, 컨텍스트는 빌더로 구성: + +```java +PolicyContext ctx = PolicyContext.builder() + .user(currentUserId) + .tenant(currentTenantId) + .resource("book", bookId) + .resourceAttributes(java.util.Map.of("ownerLoginId", book.getOwnerLoginId())) + .build(); + +access.check(Permission.of("book.read"), "book-owner", ctx); +``` + +**코드 없이 정책을 검증**하려면 admin 콘솔의 **Policies** 페이지(또는 +`POST /admin/api/v1/policies/test`)에서 정책을 고르고 주체/자원/환경을 채우면 +`PERMIT` / `DENY` / `NOT_APPLICABLE` 과 이유·매칭 규칙을 돌려줍니다. +[Access 가이드](../guides/access.md) 참고. + +--- + +## 끝났습니다 🎉 + +이제 동작하는 플랫폼 앱이 생겼습니다. 다음으로: + +- **[멀티테넌시](../guides/tenancy.md)** — 리졸버, `single` vs `multi` +- **[Access (RBAC + ABAC)](../guides/access.md)** — 그룹, 전체 정책 모델 +- **[동적 메뉴](../guides/menus.md)** · **[감사 로깅](../guides/audit.md)** · **[캐시](../guides/cache.md)** +- **[관리자 REST API](../reference/admin-api.md)** · **[설정 레퍼런스](../reference/configuration.md)** +- 완전히 실행 가능한 예제 앱: + [`devslab-kit-sample-app`](https://github.com/devslab-kr/devslab-kit/tree/main/devslab-kit-sample-app). diff --git a/docs/getting-started/tutorial.md b/docs/getting-started/tutorial.md new file mode 100644 index 0000000..6d53247 --- /dev/null +++ b/docs/getting-started/tutorial.md @@ -0,0 +1,349 @@ +# Tutorial: from zero to a running app + +This is a complete, copy-paste walkthrough for someone who has **never used +devslab-kit**. By the end you'll have a Spring Boot app with login, an admin user, +roles & permissions, a permission-protected endpoint of your own, tenant-scoped +data, and an ABAC policy — all running locally. + +No prior knowledge of the kit is assumed. Every command and file is shown in full. + +!!! info "What you need first" + - **JDK 21** (`java -version` should print 21). + - **Docker** (to run PostgreSQL — `docker info` should succeed). + - A terminal. An IDE (IntelliJ / VS Code) is nice but not required. + +--- + +## Step 1 — Create a Spring Boot project + +Generate a minimal Spring Boot 4 project (e.g. at [start.spring.io](https://start.spring.io) +choose Gradle + Java 21 + Spring Boot 4.x), or just create these two files in an +empty folder `myapp/`. + +**`settings.gradle.kts`** + +```kotlin +rootProject.name = "myapp" +``` + +**`build.gradle.kts`** + +```kotlin +plugins { + java + id("org.springframework.boot") version "4.0.6" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "com.example" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { languageVersion = JavaLanguageVersion.of(21) } +} + +repositories { mavenCentral() } + +dependencies { + // The platform: authentication, RBAC + groups + ABAC, multi-tenancy, + // dynamic menus, audit logging, and an admin REST API — all auto-configured. + implementation("kr.devslab:devslab-kit-spring-boot-starter:0.4.2") + + // devslab-kit is unopinionated about which Spring starters you bring. + // For this tutorial we want web + security + JPA + Flyway + PostgreSQL. + implementation("org.springframework.boot:spring-boot-starter-webmvc") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.springframework.boot:spring-boot-starter-flyway") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.flywaydb:flyway-database-postgresql") + runtimeOnly("org.postgresql:postgresql") + + // Lets `bootRun` start the Postgres container in compose.yaml automatically. + developmentOnly("org.springframework.boot:spring-boot-docker-compose") +} +``` + +Add the Gradle wrapper if you don't have one: `gradle wrapper` (or copy `gradlew` +from any Spring project). + +You also need a main class — **`src/main/java/com/example/myapp/MyappApplication.java`**: + +```java +package com.example.myapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MyappApplication { + public static void main(String[] args) { + SpringApplication.run(MyappApplication.class, args); + } +} +``` + +--- + +## Step 2 — Start PostgreSQL with Docker + +The kit stores everything in PostgreSQL. Create **`compose.yaml`** in the project root: + +```yaml +services: + postgres: + image: 'postgres:16-alpine' + environment: + - 'POSTGRES_DB=myapp' + - 'POSTGRES_USER=myapp' + - 'POSTGRES_PASSWORD=myapp' + ports: + - '5432:5432' +``` + +Because you added `spring-boot-docker-compose`, **Spring Boot starts this container +for you** when you run the app — you don't need to `docker compose up` yourself. + +--- + +## Step 3 — Configure the app + +Create **`src/main/resources/application.yml`**: + +```yaml +spring: + datasource: + url: jdbc:postgresql://localhost:5432/myapp + username: myapp + password: myapp + jpa: + open-in-view: false + hibernate: + ddl-auto: validate # the kit owns its schema via Flyway; don't let Hibernate touch it + +devslab: + kit: + tenant: + mode: single # one tenant for the whole app + resolver: fixed + default-tenant-id: default + identity: + jwt: + secret: dev-only-change-me-32-bytes-minimum! # ≥ 32 bytes; use a secret in prod + ttl: PT8H # access token lifetime (ISO-8601 duration) + cache: + type: in-memory # in-memory | redis | none + bootstrap: + enabled: true # provision the first admin on first boot + admin-login-id: admin + admin-password: admin # dev only — see the warning below + must-change-password: false +``` + +!!! warning "These are dev-only values" + 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). + +--- + +## Step 4 — Run it + +```bash +./gradlew bootRun +``` + +On first start the kit: + +1. starts the Postgres container (via Docker Compose), +2. runs **Flyway** to create its `platform_*` tables (on a dedicated history table, + so your own future migrations under `db/migration` won't collide), +3. **bootstraps** a tenant, a `PLATFORM_ADMIN` role with every `admin.*` permission, + and an `admin` user, +4. serves the **admin REST API** at `/admin/api/v1/**` and **Swagger UI** at + `/swagger-ui.html`. + +Leave it running and open a second terminal for the next steps. + +--- + +## Step 5 — Log in + +Every admin call needs a token. Log in as the bootstrap admin: + +```bash +curl -s localhost:8080/admin/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","loginId":"admin","rawPassword":"admin"}' +``` + +The response contains a JWT — copy it into a shell variable so the next commands can reuse it: + +```bash +TOKEN=$(curl -s localhost:8080/admin/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","loginId":"admin","rawPassword":"admin"}' | sed -E 's/.*"token":"([^"]+)".*/\1/') +echo "$TOKEN" +``` + +!!! tip "Prefer a UI?" + Point the [admin console](https://github.com/devslab-kr/devslab-kit-admin-ui) at + `http://localhost:8080` and do all of Step 6 by clicking instead of curl. + +--- + +## Step 6 — Create a permission, a role, and a user + +A **permission** is a string code (`resource.action`). A **role** is a bundle of +permissions. A **user** holds roles. Let's give a new user the ability to read books. + +```bash +# 1) create a permission -> note its "id" in the response +curl -s -X POST localhost:8080/admin/api/v1/permissions \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"code":"book.read","description":"Read books"}' + +# 2) create a role -> note its "id" +curl -s -X POST localhost:8080/admin/api/v1/roles \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","code":"LIBRARIAN","name":"Librarian"}' + +# 3) grant the permission to the role (use the ids from steps 1 & 2) +curl -s -X POST "localhost:8080/admin/api/v1/roles//permissions/" \ + -H "Authorization: Bearer $TOKEN" + +# 4) create a user +curl -s -X POST localhost:8080/admin/api/v1/users \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","loginId":"alice","rawPassword":"alice-password","email":"alice@example.com"}' + +# 5) assign the role to the user (ids from steps 2 & 4) +curl -s -X POST "localhost:8080/admin/api/v1/roles//users/?tenantId=default" \ + -H "Authorization: Bearer $TOKEN" +``` + +Now `alice` can log in (Step 5 with her credentials) and holds `book.read`. + +--- + +## Step 7 — Protect your own endpoint + +The kit exposes a `PermissionChecker` bean. Inject it and gate your code: + +```java +package com.example.myapp; + +import kr.devslab.kit.access.Permission; +import kr.devslab.kit.access.PermissionChecker; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +class BookController { + + private final PermissionChecker access; + + BookController(PermissionChecker access) { + this.access = access; + } + + @GetMapping("/api/books") + String listBooks() { + // Throws PermissionDeniedException (-> 403) if the caller lacks it. + access.check(Permission.of("book.read")); + return "here are the books"; + } +} +``` + +Call it with alice's token → `200`; with a user who lacks `book.read` → `403`. +Other methods: `hasPermission(...)`, `hasAnyPermission(...)`, `hasAllPermissions(...)`. + +--- + +## Step 8 — Know who's calling, and scope data to the tenant + +Two more beans you'll use constantly: + +```java +import kr.devslab.kit.identity.CurrentUserProvider; +import kr.devslab.kit.tenant.TenantContextHolder; + +// who is the authenticated user? +String loginId = currentUserProvider.current() + .map(u -> u.loginId()) + .orElseThrow(); + +// which tenant is this request for? (always present — single-tenant resolves "default") +String tenantId = tenantContextHolder.current() + .map(ctx -> ctx.tenantId().value()) + .orElseThrow(); +``` + +Store `tenantId` on your own entities and filter every query by it — that's all +multi-tenancy needs. Your code is identical whether you run in `single` or `multi` +mode (see the [Multi-tenancy guide](../guides/tenancy.md)). + +--- + +## Step 9 — Add an attribute-based (ABAC) rule + +RBAC answers "does the user hold `book.read`?". ABAC adds "…**for this specific +book, right now?**". You write a **`Policy`** bean; the kit collects it and +dispatches by name. + +```java +package com.example.myapp; + +import kr.devslab.kit.access.policy.Policy; +import kr.devslab.kit.access.policy.PolicyContext; +import kr.devslab.kit.access.policy.PolicyDecision; +import org.springframework.stereotype.Component; + +@Component +class BookOwnerPolicy implements Policy { + + @Override public String name() { return "book-owner"; } + + @Override + public PolicyDecision evaluate(PolicyContext ctx) { + // e.g. only the owner may touch the book + Object owner = ctx.resourceAttributes().get("ownerLoginId"); + boolean isOwner = ctx.userId().isPresent() && /* compare to owner */ owner != null; + return isOwner ? PolicyDecision.PERMIT : PolicyDecision.DENY; + } +} +``` + +Gate with the ABAC-aware overload of `check`, building the context with the builder: + +```java +PolicyContext ctx = PolicyContext.builder() + .user(currentUserId) + .tenant(currentTenantId) + .resource("book", bookId) + .resourceAttributes(java.util.Map.of("ownerLoginId", book.getOwnerLoginId())) + .build(); + +access.check(Permission.of("book.read"), "book-owner", ctx); +``` + +**Verify a policy without writing any code** from the admin console's **Policies** +page (or `POST /admin/api/v1/policies/test`): pick the policy, fill in a subject / +resource / environment, and it returns `PERMIT` / `DENY` / `NOT_APPLICABLE` with the +reason and matched rules. See the [Access guide](../guides/access.md#abac-policies). + +--- + +## You're done 🎉 + +You now have a running platform app. Where to go next: + +- **[Multi-tenancy](../guides/tenancy.md)** — resolvers, `single` vs `multi`. +- **[Access (RBAC + ABAC)](../guides/access.md)** — groups, the full policy model. +- **[Dynamic menus](../guides/menus.md)** · **[Audit logging](../guides/audit.md)** · **[Caching](../guides/cache.md)** +- **[Admin REST API](../reference/admin-api.md)** · **[Configuration reference](../reference/configuration.md)** +- A complete, runnable example app lives in + [`devslab-kit-sample-app`](https://github.com/devslab-kr/devslab-kit/tree/main/devslab-kit-sample-app). diff --git a/docs/guides/access.ko.md b/docs/guides/access.ko.md index 15c82da..27166df 100644 --- a/docs/guides/access.ko.md +++ b/docs/guides/access.ko.md @@ -39,32 +39,51 @@ class UserAdminService { ## ABAC 정책 RBAC는 "이 사용자가 권한을 가졌는가?"에 답합니다. ABAC는 더 세밀한 "…*이 특정 리소스에 -대해, 지금?*"에 답합니다. `PolicyEvaluator` SPI를 구현하면(기본은 모두 허용) kit이 -이를 참조합니다: +대해, 지금?*"에 답합니다. **`Policy`** 빈을 하나 이상 구현하면, kit의 +`DefaultPolicyEvaluator`가 모든 `Policy` 빈을 모아 `name()`으로 디스패치합니다. +(해당 이름의 정책이 없으면 평가 결과는 `NOT_APPLICABLE`.) ```java -import kr.devslab.kit.access.policy.*; - -@Bean -PolicyEvaluator policyEvaluator() { - return new PolicyEvaluator() { - @Override public PolicyDecision evaluate(PolicyContext ctx) { - // ctx.action(), ctx.resourceType(), ctx.subjectAttributes(), - // ctx.environment() … 을 보고 permit / deny PolicyDecision 반환 - ... - } - @Override public List policies() { return List.of(); } - }; +import java.util.Map; +import kr.devslab.kit.access.policy.Policy; +import kr.devslab.kit.access.policy.PolicyContext; +import kr.devslab.kit.access.policy.PolicyDecision; +import org.springframework.stereotype.Component; + +@Component +class DocOwnerPolicy implements Policy { + + @Override public String name() { return "doc-owner"; } + + @Override + public PolicyDecision evaluate(PolicyContext ctx) { + // ctx 제공: userId(), tenantId(), resourceType(), resourceId(), + // resourceAttributes(), environmentAttributes() + Object owner = ctx.resourceAttributes().get("ownerLoginId"); + return owner != null /* && 현재 사용자와 일치 */ + ? PolicyDecision.PERMIT + : PolicyDecision.DENY; + } } ``` -그런 다음 ABAC 인지 오버로드로 게이트합니다: +그런 다음 ABAC 인지 오버로드로 게이트하고, 컨텍스트는 빌더로 구성합니다: ```java -access.check(Permission.of("doc.read"), "doc-owner-policy", - new PolicyContext(userId, tenantId, Map.of(), "read", "doc", docId, Map.of(), Map.of())); +PolicyContext ctx = PolicyContext.builder() + .user(userId) + .tenant(tenantId) + .resource("doc", docId) + .resourceAttributes(Map.of("ownerLoginId", doc.ownerLoginId())) + .build(); + +access.check(Permission.of("doc.read"), "doc-owner", ctx); ``` +이유 + 매칭된 규칙까지 담은 풍부한 결과(테스트 엔드포인트에 노출됨)가 필요하면 +`evaluateDetailed(PolicyContext)`를 오버라이드해 `PolicyEvaluation`을 반환하세요 — +예: `PolicyEvaluation.deny("소유자 아님", List.of("ownership"))`. + 부작용 없이 `(subject, action, resource)` 튜플을 드라이런하려면 관리자 API의 `policies` 엔드포인트를 쓰세요 — [관리자 REST API](../reference/admin-api.md) 참고. diff --git a/docs/guides/access.md b/docs/guides/access.md index ba9eeb3..22f233b 100644 --- a/docs/guides/access.md +++ b/docs/guides/access.md @@ -40,32 +40,52 @@ the union of their direct roles and their groups' roles. ## ABAC policies RBAC answers "does this user hold the permission?". ABAC answers the finer -"…*for this specific resource, right now?*". Implement the `PolicyEvaluator` SPI -(the default permits everything) and the kit consults it: +"…*for this specific resource, right now?*". You implement one or more **`Policy`** +beans; the kit's `DefaultPolicyEvaluator` collects every `Policy` bean and dispatches +by its `name()`. (If no policy is registered for a name, evaluation returns +`NOT_APPLICABLE`.) ```java -import kr.devslab.kit.access.policy.*; - -@Bean -PolicyEvaluator policyEvaluator() { - return new PolicyEvaluator() { - @Override public PolicyDecision evaluate(PolicyContext ctx) { - // inspect ctx.action(), ctx.resourceType(), ctx.subjectAttributes(), - // ctx.environment() … and return a permit / deny PolicyDecision - ... - } - @Override public List policies() { return List.of(); } - }; +import java.util.Map; +import kr.devslab.kit.access.policy.Policy; +import kr.devslab.kit.access.policy.PolicyContext; +import kr.devslab.kit.access.policy.PolicyDecision; +import org.springframework.stereotype.Component; + +@Component +class DocOwnerPolicy implements Policy { + + @Override public String name() { return "doc-owner"; } + + @Override + public PolicyDecision evaluate(PolicyContext ctx) { + // ctx exposes: userId(), tenantId(), resourceType(), resourceId(), + // resourceAttributes(), environmentAttributes() + Object owner = ctx.resourceAttributes().get("ownerLoginId"); + return owner != null /* && owner equals the current user */ + ? PolicyDecision.PERMIT + : PolicyDecision.DENY; + } } ``` -Then gate with the ABAC-aware overload: +Then gate with the ABAC-aware overload of `check`, building the context with the builder: ```java -access.check(Permission.of("doc.read"), "doc-owner-policy", - new PolicyContext(userId, tenantId, Map.of(), "read", "doc", docId, Map.of(), Map.of())); +PolicyContext ctx = PolicyContext.builder() + .user(userId) + .tenant(tenantId) + .resource("doc", docId) + .resourceAttributes(Map.of("ownerLoginId", doc.ownerLoginId())) + .build(); + +access.check(Permission.of("doc.read"), "doc-owner", ctx); ``` +For a richer answer (a reason + which rules matched, surfaced by the test endpoint), +override `evaluateDetailed(PolicyContext)` and return a `PolicyEvaluation` — +e.g. `PolicyEvaluation.deny("not the owner", List.of("ownership"))`. + You can dry-run a `(subject, action, resource)` tuple without side effects via the admin API's `policies` endpoint — see [Admin REST API](../reference/admin-api.md). diff --git a/mkdocs.yml b/mkdocs.yml index 539142a..eb40f7f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -70,6 +70,7 @@ plugins: Getting Started: 시작하기 Installation: 설치 Quick Start: 빠른 시작 + Tutorial (zero to running): 튜토리얼 (0에서 실행까지) Guides: 가이드 Multi-tenancy: 멀티테넌시 Access (RBAC + ABAC): 접근 제어 (RBAC + ABAC) @@ -134,6 +135,7 @@ nav: - Getting Started: - Installation: getting-started/installation.md - Quick Start: getting-started/quick-start.md + - Tutorial (zero to running): getting-started/tutorial.md - Guides: - Multi-tenancy: guides/tenancy.md - Access (RBAC + ABAC): guides/access.md