FastAPI: fastapi-permissions 를 이용한 접근제어

2024. 12. 1. 17:58·탐구 생활/FastAPI

fastapi-permissions

fastapi-permissions는 Pyramid 프레임워크의 Pyraid Securiy 기능에서 영감을 받아 만들어진 라이브러리이다. 2024년 12월 기준으로 마지막 업데이트가 4년 전이어서 최신 FastAPI와의 호환성에 의문이 들지만, 접근 방식이 흥미로워 살펴보게 되었다.

 

fastapi-permissions 가 갖는 주요 철학과 개념, 내구 동작 방식을 살펴본다.


1. 주요 철학

fastapi-permissions 가 FastAPI security 와는 다른 점은 scope 로 접근을 제어하는게 아니라 더 저수준에서 세밀하게 접근을 제어한다는 것이다. 그에따라 접근자의 권한뿐만 아니라 접근 하려하는 리소스의 상태에 따라서도 접근을 제어할 수 있다.


2. 주요 개념

FastAPI에는 기본적으로 제공되지 않는 4가지 새로운 개념이 도입되었다. 

  • Resource (리소스): 관리 대상 엔티티, Access Control List 를 제공한다.
  • ACL (Access Control List, 접근 제어 리스트): 리소스별 권한 규칙 집합, (액션, 주체, 권한) 들의 집합이다.
  • Principals (주체): 사용자 및 그룹, 정의에 제한을 두지 않는다.
  • Permissions (권한): 리소스에서 수행할 수 있는 작업, 정의에 제한을 두지 않는다.

주요개념들의 이름과 역할만 들어서 어떤식으로 접근제어를 하는지 감이 잡힌다. "특정 리소스의 권한 규칙이 있을때, 접근 주체가 권한을 가지고 있는지 검증한다." 


2.1. Resource & ACL

이 예시에서 관리대상 엔티티(Resource)는 User 이다. User 클래스에 __acl__을 구현하였다.

from typing import List
from fastapi_permissions import Allow, Authenticated

# 사용자 데이터 모델 (ACL 포함)
class UserInDB:
    def __init__(self, id: int, email: str, full_name: str, roles: List[str]):
        self.id = id
        self.email = email
        self.full_name = full_name
        self.roles = roles

    def __acl__(self):
        return [
            (Allow, Authenticated, "view"),
            (Allow, f"user:{self.id}", ("edit", "delete")),
            (Allow, "role:admin", ("view", "edit", "delete")),
        ]
    
user_db: List[UserInDB] = [
    UserInDB(id=i, email=f"mail{i}@gmail.com", full_name=f"name_{i}", roles=["user"]) for i in range(10)
]

2.2. Principl & Permissions

구현된 접근 제어 리스트(acl) 이 반환하는 tuple 중 하나를 접근 제어 항목(Access Control Item) 이라고 부르겠다. 하나의 접근제어 항목은 다음과 같이 구성되어 있다.

 

(액션, 주체(Principal), 권한(Permission)), 따라서 (Allow, Authenticated, "view") 라는 접근 제어 항목은 어떤 방식으로든 Authenticated 되었다고 판단된 주체는 "view" 권한이 허가(Allow) 된다는 것을 의미한다.

 

각각의 항목을 조금 자세히 살펴보면,

  • 액션: Allow 혹은 Deny 가 가능하다.
  • 주체: 라이브러리에서 이미 정해놓은 EveryOne, Authenticated 를 제외한 나머지는 개발자가 정의하여 넣을 수 있다.
  • 권한: 라이브러리에서 정해진게 없다. 개발자가 원하는대로 설정할 수 있다.

2.3 에제 코드

순서상 주요객체와 함수를 설명해야겠지만 개인적으로 새로운 내용을 학습하고나서 그게 구현된 코드를 보는게 도움이 되었다. 그래서 이상의 개념들을 활용해서 간단한 API 와 접근제어를 구현하였다. 유저 정보를 DB 에서 읽어와서 pydantic model 로 변환하여 반환하는 것처럼 동작하는 GET 엔드포인트 하나다.

 

아래 코드를 직접 python 파일에 넣고 필요한 의존성을 설치한 뒤  http://localhost:8000/users/1 등을 해보면 유저의 정보가 조회되는 것을 확인할 수 있다.

from typing import List, Optional

from fastapi import FastAPI, HTTPException, status
from fastapi_permissions import Allow, Authenticated, configure_permissions, Everyone
from pydantic import BaseModel


# 사용자 데이터 모델 (ACL 포함)
class UserInDB:
    def __init__(self, id: int, email: str, full_name: str, roles: List[str]):
        self.id = id
        self.email = email
        self.full_name = full_name
        self.roles = roles

    def __acl__(self):
        return [
            (Allow, Authenticated, "view"),
            (Allow, f"user:{self.id}", ("edit", "delete")),
            (Allow, "role:admin", ("view", "edit", "delete")),
        ]

# 사용자 읽기 모델 (Pydantic)
class UserRead(BaseModel):
    id: int
    email: str
    full_name: str
    roles: List[str]

# 데이터베이스 시뮬레이션
user_db: List[UserInDB] = [
    UserInDB(id=i, email=f"mail{i}@gmail.com", full_name=f"name_{i}", roles=["user"]) for i in range(10)
]

# 사용자 인증 및 주체(principals) 생성
def get_active_principals(user_id: int) -> List[str]:
    # 데이터베이스에서 사용자 조회
    user = next((u for u in user_db if u.id == user_id), None)
    if user:
        principals = [Everyone, Authenticated, f"user:{user.id}"] + [f"role:{role}" for role in user.roles]
        principals.extend(getattr(user, "principals", []))
    else:
        principals = [Everyone]

    return principals

# 특정 사용자 리소스 반환
def get_user_resource(user_id: int) -> Optional[UserInDB]:
    user = next((u for u in user_db if u.id == user_id), None)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found",
        )
    return user

# 권한 검사 시스템 설정
Permission = configure_permissions(get_active_principals)

# FastAPI 앱 초기화
app = FastAPI()

# 사용자 정보 조회 엔드포인트
@app.get("/users/{user_id}", response_model=UserRead)
async def get_user(
    user: UserInDB = Permission("view", get_user_resource),
):
    return UserRead(**user.__dict__)

 

get_active_principals(user_id: int)는  user_id 가 user_db 리스트에 있으면 user 정보를 활용해 principal 을 생성해 반환한다.

 

get_user_resource(user_id: int) 는 user_id 가 user_db 리스트에 있으면 user 정보를 반환한다. 

 

그리고 아직 뭔지 모르는 Permission 이라는 객체는 configure_permissions(get_active_principals) 를 통해 생성되더니, 엔드포인트 파라미터에서는 객체 자체를 주입하고 "view" (요구되는 permission 일 것이다) 와 get_user_resource 를 이용해 구현체를 반환한다.

 

적절한 유저 정보를 반환한다는 사실과, get_active_principals 가 EveyOne 만 반환하게 바꾸면 엔드포인트가 403을 반환하다는 사실에 기반하여  어떤식으로든 Permission 객체는 user_id 를 읽어와서 get_user_resource 와 get_active_principals 를 호출하여 접근 제어와 유저정보 반환을 모두 수행한다는 것을 추측할 수 있다. 

 

이제 직관적인 흐름이 그려졌다. 이제 세부적으로 파고들어가서 어떻게 이런 일이 가능한건지 알아보자.


3. fastapi-permission 내부동작 파헤치기

내부 동작을 세부적으로 파고들기에 앞서 결론부터 요약하자면 configure_permissions 함수를 통해 만들어낸 Permissions 이라는 객체는 내부적으로 has_permission 을 호출하여 접근주체(principal) 자원(resource)에 접근권한(permission) 이 있는지 체크하는 것으로 동작한다. 이때 fastapi 에서 제공하는 Depends 를 활용하여 동작을 추상화한다.

fastapi-permissions 동작 요약

 

이제 fastapi-permissions 라이브러리 README.md 에서 언급한 함수와 객체 위주로 코드를 까보자


3.1. 주요 객체와 함수

has_permissions(principals, permission, resource):

주어진 사용자(principals)가 특정 자원(resource) 에 대한 권한(permission)이 있는지 검사하여 True/False 를 반환한다.

 

list_permissions(princiapals, resource):

주어진 사용자(principals)가 특정 자원(resource) 에 대해 가질 수 있는 모든  권한(permission)을 반환다. dict를 반환한다.

 

Permission:

FastAPI 경로(Path Operation)에서 Depends 를 사용하여 권한 검사를 간단히 처리할 수 있도록 도와주는 의존성 객체이다.

내부적으로 이미 Depends 로 감싸져있기 때문에 별도로 Depneds 를 설정할 필요는 없다.

명시적으로는 confiture_permissions() 를 이용하여 생성하고, 내부적으로는 permission_dependency_factory() 가 생성한 함수를 사용하여 동작한다.

 

permission_dependency_factory(permission, resource, active_principals_func, permission_exception): 

권한 검사를 위한 FastAPI 의존성(dependency)을 생성한다.

내부적으로 principals 와 resource 를 가져와 has_permissions() 를 호출하여 권한을 검사한다.

사용자(principals) 이 접근 권한이 없다고 판단되면 permission_exception 을 일으킨다. (default 값 변경가능)

 

configure_permissions(active_principals_func, permission_execption):

permission_dependency_factory 를 단순화한 버전이다.

 

대략적으로 살펴봤을때 중요한 역할을 하는 함수는 permission_dependency_factory() 와 configure_permissions() 으로 보인다. 이들을 중점적으로 살펴보자


3.2. permission_dependency_factory():

소스코드를 까보자

def permission_dependency_factory(
    permission: str,
    resource: Any,
    active_principals_func: Any,
    permission_exception: HTTPException,
):
    if callable(resource):
        dependable_resource = Depends(resource)
    else:
        dependable_resource = Depends(lambda: resource)

    # to get the caller signature right, we need to add only the resource and
    # user dependable in the definition
    # the permission itself is available through the outer function scope
    def permission_dependency(
        resource=dependable_resource, principals=active_principals_func
    ):
        if has_permission(principals, permission, resource):
            return resource
        raise permission_exception

    return Depends(permission_dependency)

검사할 권한(permission) 과 대상 자원(resource)를 전달받아서 의존성 주입을 받도록 만들고, 전달받은 접근자 정보를 얻는 방법(active_principals_func)를 활용하여 has_permission 으로  권한을 검사하는 permission_dependency 라는 함수를 정의하여 반환한다.

 

위의 예제코드에서 get_user_resource 가 의도한대로 동작할 수 있었던 이유는 FastAPI 엔드포인트에 의존성 (Depends) 으로 설정되어 주입됐기 때문이다.

 

그렇다면 active_principals_func 는 어떻게 그게 가능했을까? 이 함수는 FastAPI 의존성(Depends) 으로 선언되어 있지 않다.

그 비밀은 다음에 자세히 살펴볼 configure_permissions() 함수에 있다.


3.3. configure_permissions():

confiture_permissions() 는 permission_dedendency_factory 를 쉽게 사용하는 편의 함수정도로 얘기했지만 사실 중요한 차이가 있다. 바로 configure_permissions() 를 이용해야 active_principals_func 에 의존성(Depends)이 설정된다는 것이다.

def configure_permissions(
    active_principals_func: Any,
    permission_exception: HTTPException = permission_exception,
):

    active_principals_func = Depends(active_principals_func)

    return functools.partial(
        permission_dependency_factory,
        active_principals_func=active_principals_func,
        permission_exception=permission_exception,
    )

 

이제 위의 예제코드에서 필요한 부분만 짤라서 살펴보자, 각 주석에 자세히 설명되어 있다.

# 권한 검사 시스템 설정, 이때 넘긴 get_active_principals 함수는 FastAPI 의존성으로 설정된다.
# Permission 객체는 permission_dependency_factory() 를 functuools.partial 에 의해 호출하는 callable 객체이다.
# functuools.partial 덕분에 외부 클로저를 활용할 수 있어서 Permission 객체에는 permission: str, resource: Any 만 전달하면 된다.
Permission = configure_permissions(get_active_principals)

app = FastAPI()

# 사용자 정보 조회 엔드포인트
@app.get("/users/{user_id}", response_model=UserRead)
async def get_user(
    user: UserInDB = Permission("view", get_user_resource),
):
	# Permission 을 호출함으로써 get_user_resource 는 FastAPI 의존성으로 설정되었다.
    # 내부적으로 get_active_principals 함수와 get_user_resource 함수가 돌아가고 
    # has_permission 을 통해 "view" 권한이 있는지 검사한다.
    return UserRead(**user.__dict__)

 

 

 

'탐구 생활 > FastAPI' 카테고리의 다른 글

FastAPI 의존성 주입, 코드를 까보자  (0) 2024.12.04
FastAPI 의존성 주입, Depends 를 알아보자  (0) 2024.12.04
오픈소스 초보자의 FastAPI 기여하기  (2) 2024.12.03
'탐구 생활/FastAPI' 카테고리의 다른 글
  • FastAPI 의존성 주입, 코드를 까보자
  • FastAPI 의존성 주입, Depends 를 알아보자
  • 오픈소스 초보자의 FastAPI 기여하기
개발프로브
개발프로브
가볍게, 오랫동안 기록하고 싶은 블로그입니다.
  • 개발프로브
    ProbeHub
    개발프로브
  • 전체
    오늘
    어제
    • 분류 전체보기 (56)
      • 탐구 생활 (47)
        • 개발 탐구 (8)
        • FastAPI CORS (3)
        • FastAPI Log (4)
        • gRPC&Python (4)
        • SpringBoot 파헤치기 (2)
        • Python Monorepo (3)
        • Python 과 zstd (2)
        • Python (4)
        • FastAPI (4)
        • Terraform (8)
        • MSA (0)
        • GraphQL (2)
        • 데이터베이스 (2)
        • 네트워크 (0)
      • 기초 지식 (9)
        • Terraform (2)
        • MSA (5)
        • K8s (2)
  • 블로그 메뉴

    • 링크

      • github
      • stackoverflow
    • 공지사항

    • 인기 글

    • 태그

      zstd
      ORM 성능 최적화
      python arn64
      백엔드 성능
      granian
      fastapi cors
      python 불변 객체
      python 성능 개선
      django 성능 개선
      brotli
      티스토리챌린지
      오블완
      FastAPI
      springboot
      fastapi logging
      PostgreSQL
      ORM 문제
      grpc
      java
      rest vs grpc
      python graviton
      gzip
      MSA
      spring 트랜잭션
      Python
      ORM 성능
      python amd64
      Terraform
      RDBMS 성능 최적화
      sqlalchemy
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.0
    개발프로브
    FastAPI: fastapi-permissions 를 이용한 접근제어
    상단으로

    티스토리툴바