← Notes

멀티테넌트 RBAC 설계 — 관리자 권한과 테넌트 격리

security

개념

싱글테넌트 [[RBAC]]는 단순하다. admin/editor/viewer 나누고 끝. 멀티테넌트가 되면 역할 계층테넌트 격리 두 축이 생긴다.

역할 계층 (누가 뭘 할 수 있나)
  PA(Platform Admin) > GA(Group Admin) > CA(Company Admin) > User

테넌트 스코프 (어디까지 볼 수 있나)
  플랫폼 전체 > 그룹 > 회사 > 본인

이 두 축은 독립적으로 적용돼야 한다. "PA니까 전부 보여주자"가 아니라 "PA지만 현재 컨텍스트(active_company)에 해당하는 데이터만 보여주자"가 맞다.

흔한 실수: 관리자의 테넌트 우회

# 잘못된 예
if is_platform_admin(user):
    services = get_all_services()        # 테넌트 무시
else:
    services = get_company_services(company)  # 테넌트 필터링

PA에게 전체 서비스를 보여주면:

  • A회사가 구독하지 않은 서비스가 A회사 대시보드에 노출됨
  • 데이터가 테넌트 경계를 넘어서 섞임
  • PA가 특정 회사 컨텍스트로 들어갔는데 다른 회사 데이터가 보임
# 올바른 예
services = get_company_services(company)  # 모든 역할에 동일 적용
# PA의 "특별함"은 관리 기능(회사 생성, 설정 변경)에만 적용

설계 원칙

1. 테넌트 필터는 역할과 무관하게 항상 적용

데이터 조회 쿼리에서 테넌트 필터를 빼는 분기를 만들지 않는다. PA든 일반 유저든 "현재 컨텍스트의 회사"에 속한 데이터만 본다.

2. 역할은 "할 수 있는 행위"만 결정

역할 할 수 있는 것 볼 수 있는 범위
PA 회사 생성/삭제, 전체 설정 현재 선택한 회사 데이터
GA 그룹 내 회사 설정 현재 그룹의 회사들
CA 자기 회사 설정 자기 회사 데이터

"행위 권한"과 "데이터 범위"를 분리해서 생각한다.

3. active_company (컨텍스트 스위칭)

PA가 여러 회사를 관리하더라도 항상 하나의 회사 컨텍스트에서 작업한다. 회사 전환은 명시적으로.

PA가 A회사 선택 → A회사 데이터만 보임
PA가 B회사로 전환 → B회사 데이터만 보임
PA 전용 관리 페이지 → 여기서만 전체 회사 목록 노출

4. 쿼리에서 테넌트를 빼는 건 "관리 기능"에서만

# 데이터 조회: 항상 테넌트 필터
def get_services(company):
    return query("WHERE company = ?", company)

# 관리 기능: PA 전용 페이지에서만 전체 조회
@require_role("PA")
def admin_list_all_companies():
    return query("SELECT * FROM company")

체크리스트

새 API를 만들 때:

  • 데이터 조회에 테넌트(company) 필터가 들어가 있나?
  • PA일 때 테넌트 필터를 우회하는 분기가 있나? → 있으면 의심
  • 관리 기능과 일반 기능이 같은 API를 공유하고 있나? → 분리 고려
  • 역할 체크와 테넌트 체크가 독립적으로 되어 있나?

Backlinks

sunshinemoon · 2026