1. Pundit의 핵심 개념: Policy와 Scope
Pundit은 크게 두 가지 레이어로 구성됩니다. * Policy (정책): “이 사용자가 이 리소스에 대해 이 동작을 수행할 수 있는가?”라는 질문에 답합니다. 예를 들어, 특정 사용자가 다른 팀의 연락처를 수정할 수 있는지, 관리자가 아닌 사용자가 리소스를 삭제할 수 있는지 등을 결정합니다. * Scope (범위): “이 사용자가 접근할 수 있는 리소스 목록은 무엇인가?”라는 질문에 답합니다. 관리자는 모든 레코드를 볼 수 있지만, 일반 상담원은 자신이 담당하는 레코드만 볼 수 있도록 데이터를 필터링합니다.
중요한 점은 Pundit에 정렬(sorting), 비즈니스 필터(status=’active’), 페이지네이션, Eager Loading과 같은 쿼리 관련 로직을 넣지 않는 것입니다. Pundit은 오직 ‘권한’에만 집중해야 합니다.
2. 요청 흐름과 동작 원리
사용자가 ‘연락처 수정’ 버튼을 클릭했을 때의 흐름은 다음과 같습니다.
1. Controller: authorize @contact 메서드를 호출합니다.
2. Pundit 탐색: @contact 객체의 클래스 이름(Contact)을 확인하고 대응하는 ContactPolicy를 찾습니다.
3. 메서드 실행: 현재 컨트롤러 액션(edit)에 대응하는 정책 메서드(edit?)를 호출합니다.
4. 결과 처리: 메서드가 true를 반환하면 액션을 계속 진행하고, false를 반환하면 Pundit::NotAuthorizedError 예외를 발생시킵니다.
이 과정에서 Pundit은 내부적으로 ContactPolicy.new(current_user, @contact)를 실행하여 정책 인스턴스를 생성합니다. 이는 Pundit이 단순히 루비 메서드를 호출하는 세련된 방식임을 보여줍니다.
3. Policy 클래스의 구조와 내부 변수
모든 정책 클래스는 기본적으로 ApplicationPolicy를 상속받습니다. 이 베이스 클래스는 초기화 시 user와 record를 인자로 받아 내부 변수로 저장합니다.
* user: 현재 로그인한 사용자 (current_user 반환값)
* record: 권한을 확인하려는 대상 객체 (모델 인스턴스 또는 클래스)
ruby
class ContactPolicy < ApplicationPolicy
def edit?
user.admin? || record.user_id == user.id
end
end
위와 같이 작성하면, 관리자이거나 해당 레코드의 소유자인 경우에만 수정 권한을 부여하게 됩니다. attr_reader를 통해 user와 record에 쉽게 접근할 수 있습니다.
4. 컬렉션 필터링을 위한 Scope 활용
인덱스(index) 액션처럼 여러 레코드를 보여줄 때는 policy_scope를 사용합니다. 이는 단순히 데이터를 가져오는 것이 아니라, 권한에 따라 데이터 범위를 제한합니다.
ruby
def index
@contacts = policy_scope(Contact)
end
이 코드는 ContactPolicy::Scope 클래스의 resolve 메서드를 호출합니다. 관리자는 scope.all을, 일반 사용자는 scope.where(user_id: user.id)를 반환받도록 설계하여 보안 사고를 방지합니다. Scope는 백그라운드 작업이나 서비스 객체에서도 독립적으로 호출하여 재사용할 수 있습니다.
5. 보안 강화를 위한 강제 검증 설정
Pundit의 가장 강력한 기능 중 하나는 권한 검증 누락을 방지하는 것입니다. ApplicationController에 after_action 설정을 추가하여 모든 액션에서 권한 검사가 수행되었는지 강제할 수 있습니다. 만약 개발자가 authorize나 policy_scope 호출을 잊었다면, Pundit은 에러를 발생시켜 이를 즉시 알립니다. 예외적으로 권한 검사가 필요 없는 공공 페이지의 경우 skip_authorization을 명시적으로 호출하여 의도를 분명히 할 수 있습니다.