Skip to content

Lazy Permissions

Lazy permissions defer dependency resolution until the permission is actually checked at request time. This is useful when a permission depends on a resource that may not always be available or valid.

The Problem

Consider a permission that checks if the current user owns a specific resource. The resource is loaded from a database using a path parameter:

from typing import Annotated
from uuid import UUID

from fastapi import Depends, Path

from fastapi_has_permissions import Permission


async def get_article(article_id: Annotated[UUID, Path()]) -> Article:
    return await db.get(Article, article_id)


class IsArticleAuthor(Permission):
    async def check_permissions(
        self,
        article: Annotated[Article, Depends(get_article)],
        current_user: CurrentUserDep,
    ) -> bool:
        return article.author_id == current_user.id

This works for routes like GET /articles/{article_id}, but what about GET /articles (list endpoint)? There's no article_id path parameter, so the dependency resolution fails with a RequestValidationError.

lazy() -- Defer Resolution

The lazy() function wraps a permission to defer its dependency resolution. Combined with skip_on_exc, it can gracefully skip the check when dependencies can't be resolved:

from fastapi.exceptions import RequestValidationError

from fastapi_has_permissions import lazy

lazy_author_check = lazy(
    IsArticleAuthor(),
    skip_on_exc=(RequestValidationError,),
)

Now you can safely use this permission on a router that includes both list and detail endpoints:

from fastapi import APIRouter, Depends

router = APIRouter(
    prefix="/articles",
    dependencies=[Depends(lazy_author_check)],
)


@router.get("")
async def list_articles():
    # IsArticleAuthor is skipped (no article_id param)
    return await db.list(Article)


@router.get("/{article_id}")
async def get_article(article_id: UUID):
    # IsArticleAuthor is evaluated normally
    return await db.get(Article, article_id)

LazyPermission Base Class

Instead of wrapping with lazy(), you can subclass LazyPermission directly:

from dataclasses import dataclass

from fastapi_has_permissions import LazyPermission


@dataclass
class IsArticleAuthor(LazyPermission):
    async def check_permissions(
        self,
        article: Annotated[Article, Depends(get_article)],
        current_user: CurrentUserDep,
    ) -> bool:
        return article.author_id == current_user.id

LazyPermission instances automatically defer dependency resolution. You can set skip_on_exc as a class-level default:

from dataclasses import field

from fastapi.exceptions import RequestValidationError

from fastapi_has_permissions import LazyPermission
from fastapi_has_permissions.types import Exceptions


class GracefulLazyPermission(LazyPermission):
    """Base class that skips on validation errors."""
    skip_on_exc: Exceptions = field(default=(RequestValidationError,), kw_only=True)


class IsArticleAuthor(GracefulLazyPermission):
    async def check_permissions(self, article: ArticleDep, user: UserDep) -> bool:
        return article.author_id == user.id

lazy() as a Decorator

You can also use lazy() as a class decorator:

from fastapi_has_permissions import Permission, lazy


@lazy
class IsArticleAuthor(Permission):
    async def check_permissions(self, article: ArticleDep, user: UserDep) -> bool:
        return article.author_id == user.id

Or with skip_on_exc:

@lazy(skip_on_exc=(RequestValidationError,))
class IsArticleAuthor(Permission):
    async def check_permissions(self, article: ArticleDep, user: UserDep) -> bool:
        return article.author_id == user.id

Usage with Composition

Lazy permissions work with boolean composition:

from fastapi import APIRouter, Depends

from fastapi_has_permissions import lazy

router = APIRouter(
    prefix="/articles",
    dependencies=[
        Depends(
            IsEditor()
            | lazy(IsArticleAuthor(), skip_on_exc=(RequestValidationError,))
            | (IsTeamLead() & lazy(BelongsToSameTeam(), skip_on_exc=(RequestValidationError,)))
        ),
    ],
)

This means: allow access if the user is an editor, or if they authored the article, or if they're a team lead and the article belongs to their team. On list endpoints where the article can't be loaded, the lazy checks are skipped and only IsEditor() and IsTeamLead() are evaluated.

Tip

Lazy permissions are essential for router-level permission declarations where the same permission set applies to both collection and resource endpoints.