RBAC vs ABAC: 세밀한 권한 관리 설계
프롤로그 - "본인 글만 수정 가능"이 생각보다 어려웠다
처음엔 간단했다. admin과 user 두 역할만 있으면 충분할 줄 알았다. admin은 모든 권한, user는 읽기만. 깔끔하고 명확했다.
그런데 실제 요구사항이 들어왔다. "사용자는 자기가 쓴 글만 수정할 수 있어야 해요." 순간 멈칫했다. user 역할에 '글 수정' 권한을 주면 모든 사용자 글을 수정할 수 있고, 안 주면 자기 글도 못 수정한다.
역할만으로는 "누가 누구의 리소스에 접근하는가"를 표현할 수 없었다. 호텔 키카드에 비유하면, "3층 모든 방 출입 가능" 같은 역할 기반 권한은 쉽지만, "내가 예약한 방만 출입 가능"이라는 조건은 키카드만으로 표현이 안 된다.
이때 처음으로 RBAC(Role-Based Access Control)과 ABAC(Attribute-Based Access Control)의 차이가 와닿았다.
Aha! 역할만으론 부족하다. 속성이 필요하다
Authentication vs Authorization
먼저 헷갈리는 개념부터 정리했다.
- Authentication(인증): "당신이 누구인지" 확인하는 것. 로그인할 때 비밀번호 입력.
- Authorization(인가): "당신이 무엇을 할 수 있는지" 결정하는 것. 로그인 후 리소스 접근 권한.
RBAC과 ABAC은 둘 다 Authorization의 방법론이다.
RBAC: 역할 기반 권한 관리
호텔 키카드 메타포가 정확하다. 프론트 데스크 직원은 마스터키(admin), 청소 직원은 전체 층 접근키(manager), 손님은 자기 방만(guest).
// RBAC 구조
interface User {
id: string;
name: string;
roles: Role[]; // 한 사용자가 여러 역할을 가질 수 있음
}
interface Role {
id: string;
name: string; // 'admin', 'manager', 'user'
permissions: Permission[];
}
interface Permission {
id: string;
resource: string; // 'posts', 'users', 'comments'
action: string; // 'create', 'read', 'update', 'delete'
}
역할에 권한을 묶어서 관리한다. 새로운 manager가 입사하면 'manager' 역할만 부여하면 끝. 권한을 일일이 설정할 필요 없다.
역할 계층(Role Hierarchy)도 구현할 수 있다. admin > manager > user 순서로, 상위 역할은 하위 역할의 모든 권한을 상속받는다.
// RBAC 권한 체크 예시
function canUpdatePost(user: User, post: Post): boolean {
return user.roles.some(role =>
role.permissions.some(perm =>
perm.resource === 'posts' && perm.action === 'update'
)
);
}
// 문제: 모든 post에 대한 update 권한을 확인할 뿐
// "내가 쓴 글인지"는 체크하지 못함
여기서 한계가 드러났다. RBAC은 "누가 무엇을 할 수 있는가"만 표현한다. "누가 누구의 무엇을 언제 어디서 할 수 있는가" 같은 조건부 권한은 표현하기 어렵다.
ABAC: 속성 기반 권한 관리
ABAC은 역할 대신 속성(Attributes)으로 권한을 결정한다. 사용자 속성, 리소스 속성, 환경 속성을 모두 고려한다.
비유하자면, 은행 금고실 출입이다. "임원 역할"만으로 들어가는 게 아니라, "임원이면서 + 업무 시간이고 + 본인 지점이고 + 2단계 인증 완료"라는 여러 속성이 모두 만족되어야 들어갈 수 있다.
// ABAC 정책 예시
interface Policy {
id: string;
name: string;
effect: 'allow' | 'deny';
conditions: Condition[];
}
interface Condition {
attribute: string; // 'user.id', 'resource.ownerId', 'environment.time'
operator: string; // 'equals', 'greaterThan', 'in'
value: any;
}
// "사용자는 자기가 쓴 글만 수정 가능" 정책
const editOwnPostPolicy: Policy = {
id: 'edit-own-post',
name: 'Edit Own Post',
effect: 'allow',
conditions: [
{ attribute: 'user.id', operator: 'equals', value: 'resource.authorId' },
{ attribute: 'resource.type', operator: 'equals', value: 'post' },
{ attribute: 'action', operator: 'equals', value: 'update' }
]
};
이제 "본인 글만 수정 가능"이 표현된다. user.id와 resource.authorId가 일치하는지 확인하면 된다.
Deep Dive: 실제 구현과 하이브리드 접근
Database Schema for RBAC
-- users 테이블
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- roles 테이블
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL, -- 'admin', 'manager', 'user'
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- permissions 테이블
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource VARCHAR(50) NOT NULL, -- 'posts', 'users'
action VARCHAR(50) NOT NULL, -- 'create', 'read', 'update', 'delete'
description TEXT,
UNIQUE(resource, action)
);
-- role_permissions: 역할에 권한 할당
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- user_roles: 사용자에게 역할 할당
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);
이 구조면 역할과 권한을 유연하게 관리할 수 있다. 새로운 권한이 추가되면 permissions 테이블에 넣고, 필요한 역할에 연결하면 된다.
Supabase RLS: ABAC 구현체
Supabase의 Row Level Security(RLS)는 ABAC의 실제 구현이다. 각 테이블에 정책을 설정해서, 행 단위로 접근을 제어한다.
-- posts 테이블
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author_id UUID REFERENCES users(id) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- RLS 활성화
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 정책 1: 모든 사용자는 모든 글을 읽을 수 있음
CREATE POLICY "Anyone can read posts"
ON posts FOR SELECT
TO authenticated, anon
USING (true);
-- 정책 2: 사용자는 자기 글만 수정 가능
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- 정책 3: 사용자는 자기 글만 삭제 가능
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
TO authenticated
USING (auth.uid() = author_id);
-- 정책 4: admin은 모든 글을 수정/삭제 가능
CREATE POLICY "Admins can do anything"
ON posts FOR ALL
TO authenticated
USING (
EXISTS (
SELECT 1 FROM user_roles ur
JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = auth.uid() AND r.name = 'admin'
)
);
auth.uid() = author_id 조건이 핵심이다. 현재 로그인한 사용자 ID와 글 작성자 ID를 비교한다. 이게 ABAC의 속성 기반 판단이다.
CASL.js: Frontend Authorization
백엔드에서 권한을 체크하는 것도 중요하지만, 프론트엔드에서 UI를 조건부로 보여주는 것도 필요하다. CASL.js가 그 역할을 한다.
import { createMongoAbility, AbilityBuilder } from '@casl/ability';
// 사용자 권한 정의
function defineAbilitiesFor(user: User) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
if (user.roles.includes('admin')) {
can('manage', 'all'); // admin은 모든 것을 할 수 있음
} else {
can('read', 'Post');
can('create', 'Post');
can('update', 'Post', { authorId: user.id }); // 자기 글만
can('delete', 'Post', { authorId: user.id }); // 자기 글만
}
return build();
}
// React 컴포넌트에서 사용
function PostActions({ post, user }) {
const ability = defineAbilitiesFor(user);
return (
<div>
{ability.can('update', 'Post', { authorId: post.authorId }) && (
<button onClick={handleEdit}>수정</button>
)}
{ability.can('delete', 'Post', { authorId: post.authorId }) && (
<button onClick={handleDelete}>삭제</button>
)}
</div>
);
}
{ authorId: post.authorId } 조건을 넘겨서 ABAC 스타일로 권한을 체크한다. 프론트엔드에서도 "내가 쓴 글인지" 판단할 수 있다.
Hybrid Approach: RBAC + ABAC
현실적으로는 둘을 섞어 쓴다. 넓은 범위의 권한은 RBAC으로, 세밀한 조건은 ABAC으로.
- RBAC: "admin은 모든 사용자 관리 가능", "manager는 자기 팀 관리 가능"
- ABAC: "사용자는 본인 프로필만 수정 가능", "업무 시간에만 민감한 데이터 접근 가능"
// 하이브리드 권한 체크
async function canAccessResource(
user: User,
resource: Resource,
action: string
): Promise<boolean> {
// 1단계: RBAC 체크 (역할 기반 넓은 권한)
const hasRolePermission = await checkRolePermission(user, resource.type, action);
if (hasRolePermission && user.roles.includes('admin')) {
return true; // admin은 무조건 통과
}
// 2단계: ABAC 체크 (속성 기반 세밀한 조건)
const policies = await getPoliciesForAction(resource.type, action);
for (const policy of policies) {
if (evaluatePolicy(policy, user, resource)) {
return policy.effect === 'allow';
}
}
return false; // 기본값은 거부
}
결국 실제로는 "역할로 큰 틀을 잡고, 정책으로 예외를 처리한다"는 흐름이 와닿았다.
When to Use Which?
RBAC을 쓸 때:
- 조직 구조가 명확하고 역할이 잘 정의되어 있을 때
- 권한이 역할에 따라 명확하게 나뉠 때
- 관리가 단순해야 할 때 (역할만 부여하면 끝)
ABAC을 쓸 때:
- "본인 것만", "특정 시간에만", "특정 위치에서만" 같은 조건부 권한이 필요할 때
- 역할이 너무 많아져서 관리가 복잡해질 때
- 동적으로 권한을 판단해야 할 때 (사용자/리소스/환경 속성 기반)
하이브리드를 쓸 때:
- 대부분의 실제 프로젝트. 역할로 큰 틀, 정책으로 세밀한 제어.
요약: 역할은 출발점, 속성은 디테일
RBAC은 권한 관리의 기본이다. 역할을 정의하고 권한을 할당하면 관리가 쉽다. 하지만 "본인 리소스만", "특정 조건에서만" 같은 세밀한 요구사항은 표현하기 어렵다.
ABAC은 사용자, 리소스, 환경의 속성을 기반으로 권한을 판단한다. 유연하지만 정책 관리가 복잡해질 수 있다.
실제로는 RBAC으로 넓은 권한을 정의하고, ABAC으로 예외와 조건을 처리하는 하이브리드 접근이 효과적이다. Supabase RLS로 데이터베이스 레벨 보안을 강화하고, CASL.js로 프론트엔드 UI를 조건부로 제어하면 견고한 권한 시스템을 만들 수 있다.
"admin/user만 있으면 되지 않을까?"라는 초기 생각은 첫 번째 복잡한 요구사항에서 무너졌다. 하지만 그 덕분에 권한 설계의 깊이를 이해하게 되었다.