Skip to content

RBAC(Role‐Based Access Control) 도입

이지호 edited this page Jan 24, 2025 · 7 revisions

📄 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 변경과 간단한 로직 수정으로 확장이 가능.
      • 단점: 초기 설계와 구조 세팅이 필요함.
  • 참고 자료

🗺️ 문제 해결 과정

  1. DB 스키마 수정
    • Role, Permission 모델 추가
    • RolePermission 모델로 다대다 관계 구성
    • 기존 UserSessionTokenRole을 연결하여, 유저가 어떤 Role을 가지고 있는지 DB에서 쉽게 조회할 수 있도록 함.
  1. 기존 서비스 로직의 ‘호스트 여부(isHost)’ 체크 제거

    • SessionAuthRepository에서 findByTokenWithPermissions()를 통해 해당 토큰이 어떤 Role을 가지고 있고, 그 Role에 어떤 Permission이 있는지 조회하도록 변경.
    • 예: permissionId === Permissions.DELETE_QUESTION 인지 여부를 확인하여 질문 삭제 권한을 판단하도록 로직 수정.
  2. 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);
      }
  3. 권한(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>;
  4. Role 종류

    export const Roles = {
      SUPER_HOST: 'SUPER_HOST',
      SUB_HOST: 'SUB_HOST',
      PARTICIPANT: 'PARTICIPANT',
    } as const;

⚙️ 트러블슈팅

  • 문제 상황: Role과 Permission을 Postgres에 적용하는 과정에서 다대다 관계 설정이 잘못되어, prisma migrate가 오류가 발생함.

  • 원인 분석: Prisma에서 다대다 관계를 구성하려고 했기 때문.

  • 해결 방법: RolePermission 모델을 생성하고, roleNamepermissionId를 복합 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를 제공할 수 있음.

위와 같은 과정을 통해 RBAC를 도입했고, 결과적으로 권한 부여/회수, 새로운 권한 추가가 한층 쉬워졌습니다.