-
Notifications
You must be signed in to change notification settings - Fork 1
RBAC(Role‐Based Access Control) 도입
프로젝트에서 사용자(토큰)마다 서로 다른 권한을 부여하기 위해, DB 구조에 Role
, Permission
모델을 추가하고, 기존 서비스 로직을 RBAC(Role-Based Access Control) 기반으로 수정했습니다. 이를 통해 특정 기능(질문 삭제, 고정, 세션 종료 등)에 대한 접근 권한을 DB에서 쉽게 관리할 수 있게 되었습니다.
저희 서비스의 권한 정책은 아래와 같습니다
권한 | 슈퍼 호스트 | 서브 호스트 | 참가자 |
---|---|---|---|
1. 세션 종료 | O | X | X |
2. 타인에게 호스트 권한 부여 | O | X | X |
3. 타인의 호스트 권한 해제 | O | X | X |
4. 질문 삭제 | O | O | X |
5. 답변 삭제 | O | O | X |
6. 질문 고정 | O | O | X |
7. 질문에 대한 답변 완료 기능 | O | O | X |
더 자세한 정보는 멀티 호스트 지원 문서를 통해 확인할 수 있습니다.
-
왜 RBAC를 도입했나?
기존에는 ‘호스트 여부(isHost)’만 판단해 권한을 부여했습니다. 하지만 점점 다양한 권한 요구사항(질문 삭제, 고정, 세션 종료 등)이 생기면서 if문이나 조건문이 복잡해졌고, 로직 확장이 어려워졌습니다.
-
해결하고자 하는 문제점
- 권한 부여 로직이 분산되어 있고, 조건문이 너무 많아 가독성이 떨어졌습니다.
- 새로운 권한 추가 시 코드 곳곳을 수정해야 했습니다.
- 호스트가 여러 명이 될 수 있는 시나리오, 일부 호스트 역할만 필요한 시나리오 등이 생겨나, 역할(Role) 기반 권한 관리가 필요했습니다.
-
관련 컨텍스트
- 팀에서 Q&A 세션을 진행하는 동안 질문 삭제, 고정, 세션 종료와 같은 기능을 세분화하고 싶어 했습니다.
- NestJS + Prisma + Postgres 스택을 사용 중이었고, Prisma 모델에
Role
,Permission
을 추가하고, NestJS 서비스 로직에서 권한을 체크하는 방식으로 접근했습니다.
-
단순 if문(호스트 여부) vs Role + Permission DB관리
-
단순 if문 방식
- 장점: 구현이 간단하고 빠름.
- 단점: 확장성이 떨어지고, 권한별 로직이 복잡해지면 유지보수 어려움.
-
Role + Permission을 DB로 관리
- 장점: 권한 종류가 늘어나거나 세분화되어도 DB 변경과 간단한 로직 수정으로 확장이 가능.
- 단점: 초기 설계와 구조 세팅이 필요함.
-
단순 if문 방식
- 참고 자료
-
DB 스키마 수정
-
Role
,Permission
모델 추가 -
RolePermission
모델로 다대다 관계 구성 - 기존
UserSessionToken
과Role
을 연결하여, 유저가 어떤 Role을 가지고 있는지 DB에서 쉽게 조회할 수 있도록 함.
-
![](https://private-user-images.githubusercontent.com/112055561/406285379-f76f3201-faf0-4b5e-91d0-a05ab6451d32.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkyMDc2NDUsIm5iZiI6MTczOTIwNzM0NSwicGF0aCI6Ii8xMTIwNTU1NjEvNDA2Mjg1Mzc5LWY3NmYzMjAxLWZhZjAtNGI1ZS05MWQwLWEwNWFiNjQ1MWQzMi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwMjEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDIxMFQxNzA5MDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kN2FhYzQyMWZhYzdlOGNlNzJiMzMxYzAwNzRjN2NjYjE4ODhlNTkzMWRlYjA1ZjBkZjY4MDU4M2YyM2YwNTEyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.D7PuxiflgF-ttjqPYXtozSZ6PvcToC9kaXf-1LjA480)
-
기존 서비스 로직의 ‘호스트 여부(isHost)’ 체크 제거
-
SessionAuthRepository
에서findByTokenWithPermissions()
를 통해 해당 토큰이 어떤 Role을 가지고 있고, 그 Role에 어떤Permission
이 있는지 조회하도록 변경. - 예:
permissionId === Permissions.DELETE_QUESTION
인지 여부를 확인하여 질문 삭제 권한을 판단하도록 로직 수정.
-
-
Before/After Code 비교
-
Before: 단순히
isHost
여부를 확인하고, 본인이 작성한 것인지 판별한 뒤, 조건에 따라 예외처리를 던지는 방식. -
After:
role.permissions.some(({ permissionId }) => permissionId === Permissions.DELETE_QUESTION)
와 같이 DB에 저장된 권한 정보를 직접 확인. -
예시(질문 삭제 로직):
async deleteQuestion(questionId: number, question: Question, { token }: BaseDto) { const { role } = await this.sessionAuthRepository.findByTokenWithPermissions(token); const granted = role.permissions.some( ({ permissionId }) => permissionId === Permissions.DELETE_QUESTION, ); if (!granted) { if (question.createUserToken !== token) { throw new ForbiddenException('권한이 없습니다.'); } // 이하 생략... } return await this.questionRepository.deleteQuestion(questionId); }
-
-
권한(Permission) 정의
export const PermissionId = { 1: 'TERMINATE_SESSION', 2: 'GRANT_HOST', 3: 'REVOKE_HOST', 4: 'DELETE_QUESTION', 5: 'DELETE_REPLY', 6: 'PIN_QUESTION', 7: 'CLOSE_QUESTION', } as const; type PermissionNumber = keyof typeof PermissionId; type PermissionDescription = (typeof PermissionId)[PermissionNumber]; export const Permissions = Object.fromEntries( Object.entries(PermissionId).map(([key, value]) => [value, Number(key)]), ) as Record<PermissionDescription, PermissionNumber>;
-
Role 종류
export const Roles = { SUPER_HOST: 'SUPER_HOST', SUB_HOST: 'SUB_HOST', PARTICIPANT: 'PARTICIPANT', } as const;
-
문제 상황: Role과 Permission을 Postgres에 적용하는 과정에서 다대다 관계 설정이 잘못되어,
prisma migrate
가 오류가 발생함. -
원인 분석: Prisma에서 다대다 관계를 구성하려고 했기 때문.
-
해결 방법:
RolePermission
모델을 생성하고,roleName
과permissionId
를 복합 Primary Key로 지정하여 다대다 관계를 정상화.model RolePermission { role Role @relation(fields: [roleName], references: [name]) roleName String permission Permission @relation(fields: [permissionId], references: [permissionId]) permissionId Int @map("permission_id") @@id([roleName, permissionId]) }
-
유연한 권한 관리
- 호스트, 서브 호스트, 일반 사용자 등 다양한 Roles를 정의할 수 있고, 권한 또한 DB에서 바로 조정 가능해졌습니다.
- 새로운 권한(예: 질문 숨기기)이 필요해도 DB에 Permission을 추가하고 로직을 한두 군데만 수정하면 되므로 관리가 쉬워졌습니다.
-
유지보수성 향상
- 모든 권한 체크 로직이 통일된 방식(
permissions.some(...)
)으로 처리되어, 코드 가독성이 좋아지고, 수정 범위도 명확해졌습니다.
- 모든 권한 체크 로직이 통일된 방식(
-
추가 개선사항
-
Frontend 연동: 클라이언트(UI)에서 질문 삭제 버튼은
DELETE_QUESTION
권한이 없는 사용자에겐 보이지 않도록 하는 등, Role/Permission 정보를 활용하여 더 풍부한 UX를 제공할 수 있음.
-
Frontend 연동: 클라이언트(UI)에서 질문 삭제 버튼은
위와 같은 과정을 통해 RBAC를 도입했고, 결과적으로 권한 부여/회수, 새로운 권한 추가가 한층 쉬워졌습니다.