diff --git a/docs/guides/access.ko.md b/docs/guides/access.ko.md index 6e4cb2e..991be01 100644 --- a/docs/guides/access.ko.md +++ b/docs/guides/access.ko.md @@ -1,50 +1,110 @@ # 접근 제어 (RBAC + ABAC) -`devslab-kit`의 인가는 두 계층입니다: +**인가(authorization)**는 누가 무엇을 할 수 있는지 정합니다. `devslab-kit`은 두 계층으로 +처리합니다: -1. **RBAC** — 사용자는 **역할**을 가지며(직접 또는 **그룹**을 통해), 역할은 **권한**을 - 부여합니다(`admin.user.read` 같은 안정적인 문자열 코드). -2. **ABAC** — RBAC 결정을 속성(주체·행위·리소스·환경)으로 더 정밀하게 다듬는 선택적 - **정책** 계층. +1. **RBAC**(역할 기반) — 거친 계층. **사용자**가 **역할**을 가지고(직접 또는 **그룹**을 통해), + 역할이 **권한**을 부여합니다 — `admin.user.read` 같은 고정 문자열 코드. "이 사용자가 X를 + 할 수 있나?" +2. **ABAC**(속성 기반) — RBAC 결정을 속성으로 더 세밀하게 다듬는 선택 계층. "그것도 *이 특정 + 리소스에, 지금* 할 수 있나?" -## 권한 확인 +대부분의 앱은 RBAC만으로 충분합니다. 권한이 *데이터*에 좌우될 때(소유자만 수정, 같은 테넌트만, +영업시간) ABAC를 씁니다. 처음이면 [튜토리얼](../getting-started/tutorial.md)부터 — 6~9단계가 +이걸 실제로 설정합니다. -`PermissionChecker`를 주입하세요. 현재 사용자를 기준으로 평가합니다: +## 개념 잡기 + +``` + ┌─ 직접 역할 ──┐ + 사용자 ──┤ ├──► 역할 ──► 권한 ← RBAC: 권한을 가졌나? + └─ 그룹 ─ 역할 ┘ + 그다음, 선택적으로: + 권한 + 리소스 속성 ──► Policy ──► PERMIT/DENY ← ABAC: 이 리소스에 대해? +``` + +사용자의 **유효 권한**은 직접 역할과 그룹 역할이 가진 권한의 합집합입니다. + +## 1단계 — 역할·권한 설정 + +권한을 정의하고, 역할로 묶고, 역할을 사용자에게 배정합니다. (최초 관리자 +[부트스트랩](bootstrap.md)이 이미 전체 `admin.*`을 가진 `PLATFORM_ADMIN`을 심어 둡니다 — 여기서는 +직접 추가하는 법입니다.) + +=== "관리자 콘솔" + + [관리자 콘솔](admin-console.md)에서: + + 1. **Permissions** → **New** → `doc.read` 같은 코드(+설명) 추가. + 2. **Roles** → **New** → 예: `editor` 생성. + 3. 역할 열기 → `doc.read`(외 필요한 것)를 **grant**. + 4. **Users** → 사용자 선택 → `editor` 역할 **assign**(또는 그 역할을 가진 **그룹**에 추가). + +=== "REST API" + + ```bash + # 1. 권한 생성 + curl -X POST localhost:8080/admin/api/v1/permissions \ + -H 'Authorization: Bearer ' -H 'Content-Type: application/json' \ + -d '{"code":"doc.read","description":"Read documents"}' + + # 2. 역할 생성 + curl -X POST localhost:8080/admin/api/v1/roles \ + -H 'Authorization: Bearer ' -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","code":"editor","name":"Editor"}' + + # 3. 역할에 권한 부여 (id는 위 응답에서) + curl -X POST localhost:8080/admin/api/v1/roles/{roleId}/permissions/{permissionId} \ + -H 'Authorization: Bearer ' + + # 4. 사용자에게 역할 배정 + curl -X POST localhost:8080/admin/api/v1/roles/{roleId}/users/{userId} \ + -H 'Authorization: Bearer ' + ``` + + `permissions`·`roles`·`groups` 리소스 전체는 [관리자 REST API](../reference/admin-api.md) 참고. + +## 2단계 — 코드에서 권한 확인 + +`PermissionChecker`를 주입합니다. 현재 사용자 기준으로 평가합니다: ```java +// src/main/java/com/example/myapp/DocService.java import kr.devslab.kit.access.PermissionChecker; import kr.devslab.kit.access.Permission; @Service -class UserAdminService { +class DocService { + private final PermissionChecker access; - UserAdminService(PermissionChecker access) { this.access = access; } + DocService(PermissionChecker access) { this.access = access; } - void deactivate(String loginId) { - access.check(Permission.of("admin.user.write")); // 없으면 PermissionDeniedException - // … + Document open(String docId) { + access.check(Permission.of("doc.read")); // 없으면 PermissionDeniedException + return load(docId); } } ``` -이 외에 `hasPermission(Permission)`, `hasAnyPermission(Permission...)`, -`hasAllPermissions(Permission...)`도 있습니다. +`hasPermission(Permission)`, `hasAnyPermission(Permission...)`, +`hasAllPermissions(Permission...)`도 있습니다 — 예외 대신 분기하고 싶을 때 사용. ## 그룹 -**그룹**은 여러 사용자를 위해 역할을 묶습니다 — 역할을 일일이 붙이는 대신 사용자를 -`eng-team`에 한 번 넣으면 됩니다. 사용자의 유효 권한은 직접 역할과 그룹 역할의 합집합입니다. +**그룹**은 사용자 집합을 위해 역할을 묶습니다 — 역할을 하나하나 붙이는 대신 사용자를 +`eng-team`에 한 번 넣으면 됩니다. 그룹(멤버 + 역할 부여)은 [관리자 콘솔](admin-console.md)이나 +`groups` REST 리소스에서 관리합니다. 사용자의 유효 권한에는 그룹의 역할이 자동 포함됩니다. -## ABAC 정책 { #abac-policies } +## 3단계 — 리소스 단위 규칙을 위한 ABAC { #abac-policies } -RBAC는 "이 사용자가 권한을 가졌는가?"에 답합니다. ABAC는 더 세밀한 "…*이 특정 리소스에 -대해, 지금?*"에 답합니다. **`Policy`** 빈을 하나 이상 구현하면, kit의 -`DefaultPolicyEvaluator`가 모든 `Policy` 빈을 모아 `name()`으로 디스패치합니다. -(해당 이름의 정책이 없으면 평가 결과는 `NOT_APPLICABLE`.) +RBAC는 "이 사용자가 권한을 가졌는가?"에 답합니다. ABAC는 더 세밀한 "…*이 특정 리소스에 대해, +지금?*"에 답합니다. 하나 이상의 **`Policy`** 빈을 구현하면, kit의 `DefaultPolicyEvaluator`가 모든 +`Policy` 빈을 모아 `name()`으로 디스패치합니다. (해당 이름의 정책이 없으면 평가는 +`NOT_APPLICABLE`을 반환합니다.) ```java -import java.util.Map; +// src/main/java/com/example/myapp/DocOwnerPolicy.java import kr.devslab.kit.access.policy.Policy; import kr.devslab.kit.access.policy.PolicyContext; import kr.devslab.kit.access.policy.PolicyDecision; @@ -60,16 +120,17 @@ class DocOwnerPolicy implements Policy { // ctx 제공: userId(), tenantId(), resourceType(), resourceId(), // resourceAttributes(), environmentAttributes() Object owner = ctx.resourceAttributes().get("ownerLoginId"); - return owner != null /* && 현재 사용자와 일치 */ + return owner != null /* && owner가 현재 사용자와 같으면 */ ? PolicyDecision.PERMIT : PolicyDecision.DENY; } } ``` -그런 다음 ABAC 인지 오버로드로 게이트하고, 컨텍스트는 빌더로 구성합니다: +그런 다음 `check`의 ABAC 오버로드로, 빌더로 컨텍스트를 만들어 게이트합니다: ```java +// DocService 안, 특정 문서 편집 시: PolicyContext ctx = PolicyContext.builder() .user(userId) .tenant(tenantId) @@ -77,14 +138,23 @@ PolicyContext ctx = PolicyContext.builder() .resourceAttributes(Map.of("ownerLoginId", doc.ownerLoginId())) .build(); -access.check(Permission.of("doc.read"), "doc-owner", ctx); +access.check(Permission.of("doc.read"), "doc-owner", ctx); // RBAC 먼저, 그다음 정책 ``` -이유 + 매칭된 규칙까지 담은 풍부한 결과(테스트 엔드포인트에 노출됨)가 필요하면 -`evaluateDetailed(PolicyContext)`를 오버라이드해 `PolicyEvaluation`을 반환하세요 — -예: `PolicyEvaluation.deny("소유자 아님", List.of("ownership"))`. +`check`는 RBAC **와** 명명된 정책을 모두 강제합니다: 사용자는 `doc.read`를 가져야 *하고* +`doc-owner` 정책이 `PERMIT`해야 합니다. + +더 풍부한 답(이유 + 어떤 규칙이 매칭됐는지, 테스트 엔드포인트가 노출)을 원하면 +`evaluateDetailed(PolicyContext)`를 오버라이드해 `PolicyEvaluation`을 반환하세요 — 예: +`PolicyEvaluation.deny("not the owner", List.of("ownership"))`. + +!!! tip "결정 dry-run" + 관리자 콘솔의 **Policies** 화면(및 `policies` 관리 엔드포인트)은 `(subject, action, resource)` + 튜플을 **부작용 없이** 평가할 수 있어, 경로에 엮기 전에 정책을 테스트할 수 있습니다. + [관리자 콘솔 가이드](admin-console.md)와 [관리자 REST API](../reference/admin-api.md) 참고. -부작용 없이 `(subject, action, resource)` 튜플을 드라이런하려면 관리자 API의 `policies` -엔드포인트를 쓰세요 — [관리자 REST API](../reference/admin-api.md) 참고. +## 더 보기 -관련 JWT·잠금 설정은 [설정 레퍼런스](../reference/configuration.md#identity)를 참고하세요. +- [관리자 콘솔](admin-console.md) — 역할·권한·그룹 관리 및 정책 테스트. +- [동적 메뉴](menus.md) — 사용자에게 권한이 허용하는 메뉴만 표시. +- [설정 레퍼런스](../reference/configuration.md#identity) — JWT + 잠금 설정. diff --git a/docs/guides/access.md b/docs/guides/access.md index 22f233b..16010ee 100644 --- a/docs/guides/access.md +++ b/docs/guides/access.md @@ -1,43 +1,106 @@ # Access (RBAC + ABAC) -Authorization in `devslab-kit` is two layers: +**Authorization** decides who may do what. `devslab-kit` does it in two layers: -1. **RBAC** — users hold **roles** (directly or via **groups**); roles grant - **permissions** (stable string codes like `admin.user.read`). -2. **ABAC** — an optional **policy** layer that refines an RBAC decision with - attributes (subject, action, resource, environment). +1. **RBAC** (role-based) — the coarse layer. A **user** holds **roles** (directly or + via **groups**); a role grants **permissions** — stable string codes like + `admin.user.read`. "Can this user do X?" +2. **ABAC** (attribute-based) — an optional fine layer that refines an RBAC decision + with attributes. "Can they do X *to this specific resource, right now*?" -## Checking permissions +Most apps need only RBAC. Reach for ABAC when a permission depends on the *data* +(owner-only edits, same-tenant-only, business hours). New here? Do the +[Tutorial](../getting-started/tutorial.md) first — Steps 6–9 set this up live. + +## The mental model + +``` + ┌─ direct roles ─┐ + user ────┤ ├──► roles ──► permissions ← RBAC: do they hold it? + └─ groups ─ roles┘ + then, optionally: + permission + resource attributes ──► Policy ──► PERMIT/DENY ← ABAC: for THIS resource? +``` + +A user's **effective permissions** are the union of their direct roles' and their +groups' roles' permissions. + +## Step 1 — Set up roles & permissions + +You define permissions, group them into roles, and assign roles to users. (The +first-admin [bootstrap](bootstrap.md) already seeds `PLATFORM_ADMIN` with the full +`admin.*` set — this is how you add your own.) + +=== "Admin console" + + In the [admin console](admin-console.md): + + 1. **Permissions** → **New** → add a code like `doc.read` (+ description). + 2. **Roles** → **New** → create e.g. `editor`. + 3. Open the role → **grant** it `doc.read` (and any others). + 4. **Users** → pick a user → **assign** the `editor` role (or add them to a + **Group** that carries it). + +=== "REST API" + + ```bash + # 1. create a permission + curl -X POST localhost:8080/admin/api/v1/permissions \ + -H 'Authorization: Bearer ' -H 'Content-Type: application/json' \ + -d '{"code":"doc.read","description":"Read documents"}' + + # 2. create a role + curl -X POST localhost:8080/admin/api/v1/roles \ + -H 'Authorization: Bearer ' -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","code":"editor","name":"Editor"}' + + # 3. grant the permission to the role (ids from the responses above) + curl -X POST localhost:8080/admin/api/v1/roles/{roleId}/permissions/{permissionId} \ + -H 'Authorization: Bearer ' + + # 4. assign the role to a user + curl -X POST localhost:8080/admin/api/v1/roles/{roleId}/users/{userId} \ + -H 'Authorization: Bearer ' + ``` + + See the [Admin REST API](../reference/admin-api.md) for the full `permissions`, + `roles` and `groups` resources. + +## Step 2 — Check permissions in code Inject `PermissionChecker`. It evaluates against the current user: ```java +// src/main/java/com/example/myapp/DocService.java import kr.devslab.kit.access.PermissionChecker; import kr.devslab.kit.access.Permission; @Service -class UserAdminService { +class DocService { + private final PermissionChecker access; - UserAdminService(PermissionChecker access) { this.access = access; } + DocService(PermissionChecker access) { this.access = access; } - void deactivate(String loginId) { - access.check(Permission.of("admin.user.write")); // throws PermissionDeniedException if missing - // … + Document open(String docId) { + access.check(Permission.of("doc.read")); // throws PermissionDeniedException if missing + return load(docId); } } ``` Also available: `hasPermission(Permission)`, `hasAnyPermission(Permission...)`, -`hasAllPermissions(Permission...)`. +`hasAllPermissions(Permission...)` — use these when you want to branch rather than +throw. ## Groups A **group** bundles roles for a set of users — assign a user to `eng-team` once -instead of attaching every role individually. A user's effective permissions are -the union of their direct roles and their groups' roles. +instead of attaching every role individually. Manage groups (members + role grants) +in the [admin console](admin-console.md) or the `groups` REST resource. A user's +effective permissions include their groups' roles automatically. -## ABAC policies +## Step 3 — ABAC for per-resource rules { #abac-policies } RBAC answers "does this user hold the permission?". ABAC answers the finer "…*for this specific resource, right now?*". You implement one or more **`Policy`** @@ -46,7 +109,7 @@ by its `name()`. (If no policy is registered for a name, evaluation returns `NOT_APPLICABLE`.) ```java -import java.util.Map; +// src/main/java/com/example/myapp/DocOwnerPolicy.java import kr.devslab.kit.access.policy.Policy; import kr.devslab.kit.access.policy.PolicyContext; import kr.devslab.kit.access.policy.PolicyDecision; @@ -69,9 +132,11 @@ class DocOwnerPolicy implements Policy { } ``` -Then gate with the ABAC-aware overload of `check`, building the context with the builder: +Then gate with the ABAC-aware overload of `check`, building the context with the +builder: ```java +// inside DocService, when editing a specific doc: PolicyContext ctx = PolicyContext.builder() .user(userId) .tenant(tenantId) @@ -79,15 +144,24 @@ PolicyContext ctx = PolicyContext.builder() .resourceAttributes(Map.of("ownerLoginId", doc.ownerLoginId())) .build(); -access.check(Permission.of("doc.read"), "doc-owner", ctx); +access.check(Permission.of("doc.read"), "doc-owner", ctx); // RBAC first, then the policy ``` +`check` enforces RBAC **and** the named policy: the user must hold `doc.read` *and* +the `doc-owner` policy must `PERMIT`. + 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). +!!! tip "Dry-run a decision" + The admin console's **Policies** screen (and the `policies` admin endpoint) can + evaluate a `(subject, action, resource)` tuple **without side effects**, so you + can test a policy before wiring it into a path. See the + [Admin Console guide](admin-console.md) and [Admin REST API](../reference/admin-api.md). + +## See also -See the [Configuration reference](../reference/configuration.md#identity) for the -related JWT and lockout settings. +- [Admin Console](admin-console.md) — manage roles, permissions, groups and test policies. +- [Dynamic Menus](menus.md) — show users only the menu items their permissions allow. +- [Configuration reference](../reference/configuration.md#identity) — JWT + lockout settings.