콘텐츠로 이동

접근 제어 (RBAC + ABAC)

인가(authorization)는 누가 무엇을 할 수 있는지 정합니다. devslab-kit은 두 계층으로 처리합니다:

  1. RBAC(역할 기반) — 거친 계층. 사용자역할을 가지고(직접 또는 그룹을 통해), 역할이 권한을 부여합니다 — admin.user.read 같은 고정 문자열 코드. "이 사용자가 X를 할 수 있나?"
  2. ABAC(속성 기반) — RBAC 결정을 속성으로 더 세밀하게 다듬는 선택 계층. "그것도 이 특정 리소스에, 지금 할 수 있나?"

대부분의 앱은 RBAC만으로 충분합니다. 권한이 데이터에 좌우될 때(소유자만 수정, 같은 테넌트만, 영업시간) ABAC를 씁니다. 처음이면 튜토리얼부터 — 6~9단계가 이걸 실제로 설정합니다.

개념 잡기

            ┌─ 직접 역할 ──┐
   사용자 ──┤              ├──► 역할 ──► 권한          ← RBAC: 권한을 가졌나?
            └─ 그룹 ─ 역할 ┘
                                      그다음, 선택적으로:
   권한 + 리소스 속성 ──► Policy ──► PERMIT/DENY        ← ABAC: 이 리소스에 대해?

사용자의 유효 권한은 직접 역할과 그룹 역할이 가진 권한의 합집합입니다.

1단계 — 역할·권한 설정

권한을 정의하고, 역할로 묶고, 역할을 사용자에게 배정합니다. (최초 관리자 부트스트랩이 이미 전체 admin.*을 가진 PLATFORM_ADMIN을 심어 둡니다 — 여기서는 직접 추가하는 법입니다.)

클릭 대신 설정으로 시드

환경마다 스타터 역할을 손으로 만들지 않으려면 devslab.kit.bootstrap.seed에 선언하세요 — kit이 부팅 시 멱등하게 생성합니다. 최초 관리자 부트스트랩 → 시드 참고.

관리자 콘솔에서:

  1. PermissionsNewdoc.read 같은 코드(+설명) 추가.
  2. RolesNew → 예: editor 생성.
  3. 역할 열기 → doc.read(외 필요한 것)를 grant.
  4. Users → 사용자 선택 → editor 역할 assign(또는 그 역할을 가진 그룹에 추가).
# 1. 권한 생성
curl -X POST localhost:8080/admin/api/v1/permissions \
  -H 'Authorization: Bearer <token>' -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 <token>' -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 <token>'

# 4. 사용자에게 역할 배정
curl -X POST localhost:8080/admin/api/v1/roles/{roleId}/users/{userId} \
  -H 'Authorization: Bearer <token>'

permissions·roles·groups 리소스 전체는 관리자 REST API 참고.

2단계 — 코드에서 권한 확인

PermissionChecker를 주입합니다. 현재 사용자 기준으로 평가합니다:

// src/main/java/com/example/myapp/DocService.java
import kr.devslab.kit.access.PermissionChecker;
import kr.devslab.kit.access.Permission;

@Service
class DocService {

    private final PermissionChecker access;

    DocService(PermissionChecker access) { this.access = access; }

    Document open(String docId) {
        access.check(Permission.of("doc.read"));   // 없으면 PermissionDeniedException
        return load(docId);
    }
}

hasPermission(Permission), hasAnyPermission(Permission...), hasAllPermissions(Permission...)도 있습니다 — 예외 대신 분기하고 싶을 때 사용.

그룹

그룹은 사용자 집합을 위해 역할을 묶습니다 — 역할을 하나하나 붙이는 대신 사용자를 eng-team에 한 번 넣으면 됩니다. 그룹(멤버 + 역할 부여)은 관리자 콘솔이나 groups REST 리소스에서 관리합니다. 사용자의 유효 권한에는 그룹의 역할이 자동 포함됩니다.

3단계 — 리소스 단위 규칙을 위한 ABAC

RBAC는 "이 사용자가 권한을 가졌는가?"에 답합니다. ABAC는 더 세밀한 "…이 특정 리소스에 대해, 지금?"에 답합니다. 하나 이상의 Policy 빈을 구현하면, kit의 DefaultPolicyEvaluator가 모든 Policy 빈을 모아 name()으로 디스패치합니다. (해당 이름의 정책이 없으면 평가는 NOT_APPLICABLE을 반환합니다.)

// 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;
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 /* && owner가 현재 사용자와 같으면 */
                ? PolicyDecision.PERMIT
                : PolicyDecision.DENY;
    }
}

그런 다음 check의 ABAC 오버로드로, 빌더로 컨텍스트를 만들어 게이트합니다:

// DocService 안, 특정 문서 편집 시:
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);   // RBAC 먼저, 그다음 정책

check는 RBAC 명명된 정책을 모두 강제합니다: 사용자는 doc.read를 가져야 하고 doc-owner 정책이 PERMIT해야 합니다.

더 풍부한 답(이유 + 어떤 규칙이 매칭됐는지, 테스트 엔드포인트가 노출)을 원하면 evaluateDetailed(PolicyContext)를 오버라이드해 PolicyEvaluation을 반환하세요 — 예: PolicyEvaluation.deny("not the owner", List.of("ownership")).

결정 dry-run

관리자 콘솔의 Policies 화면(및 policies 관리 엔드포인트)은 (subject, action, resource) 튜플을 부작용 없이 평가할 수 있어, 경로에 엮기 전에 정책을 테스트할 수 있습니다. 관리자 콘솔 가이드관리자 REST API 참고.

더 보기