Ir para o conteúdo

Permissões

O Lilya incorpora um sistema de permissões inato desenhado para facilitar o princípio de separation of concerns. Interessantemente, esse sistema de permissões é muito parecido com os middlewares.

Essencialmente, as permissões no Lilya funcionam como aplicações ASGI puros, semelhantes a middlewares, mas são especificamente desenvolvidas para gerir acessos à aplicação.

A razão para a introdução de uma nova aplicação ṕarecida à ASGI, mas para permissões, reside em manter um propósito claro e singular para cada componente. O Lilya garante essa distinção.

As permissões operam na sequência após o middleware e antes de chegar ao handler, posicionando-as idealmente para controlar o acesso à aplicação.

Usando a permissão

A aplicação Lilya fornece uma forma de incluir a permissão ASGI de forma a garantir que permanece encapsulada no exception handler.

from lilya.apps import Lilya
from lilya.exceptions import PermissionDenied
from lilya.permissions import DefinePermission
from lilya.protocols.permissions import PermissionProtocol
from lilya.requests import Request
from lilya.responses import Ok
from lilya.routing import Path
from lilya.types import ASGIApp, Receive, Scope, Send


class AllowAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-admin" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()


def user(user: str):
    return Ok({"message": f"Welcome {user}"})


app = Lilya(
    routes=[Path("/{user}", user)],
    permissions=[DefinePermission(AllowAccess)],
)

Ao definir uma permissão, é imprescindível utilizar o lilya.permissions.DefinePermission para encapsulá-la. Além disso, é aconselhável seguir o PermissionProtocol do lilya.protocols.permissions.PermissionProtocol, pois fornece uma interface para a definição.

O Lilya inclui uma excepção por defeito especificamente para negar permissões. Tipicamente, ao negar uma permissão, é mandado um statuc code 403 junto com uma mensagem específica. Essa funcionalidade é encapsulada no lilya.exceptions.PermissionDenied.

Além disso, os detalhes da mensagem podem ser personalizados conforme necessário.

PermissionProtocol

Para aqueles vindos de uma linguagem com static type como Java ou C#, um protocolo é equivalente em Python a uma interface.

O PermissionProtocol é simplesmente uma interface para criar permissões para o Lilya, mediante a aplicação da implementação do __init__ e do async def __call__.

O desenho deste protocolo também está alinhado com a Permissão Pura ASGI.

Exemplo rápido

from lilya.exceptions import PermissionDenied
from lilya.protocols.permissions import PermissionProtocol
from lilya.requests import Request
from lilya.types import ASGIApp, Receive, Scope, Send


class DenyAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        raise PermissionDenied()


class AllowAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-admin" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()

Permissão e o aplicação

Ao criar esse tipo de permissões, também garante que os protocolos sejam seguidos, reduzindo assim os erros de desenvolvimento por meio da remoção de erros comuns.

É muito simples adicionar middlewares à aplicação. Podem ser incluídos em qualquer nível da aplicação. Podem ser inseridos no Lilya/ChildLilya, Include, Path e WebSocketPath.

from lilya.apps import Lilya
from lilya.exceptions import PermissionDenied
from lilya.permissions import DefinePermission
from lilya.protocols.permissions import PermissionProtocol
from lilya.requests import Request
from lilya.types import ASGIApp, Receive, Scope, Send


class AllowAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-admin" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()


app = Lilya(
    routes=[...],
    permissions=[
        DefinePermission(AllowAccess),
    ],
)
from lilya.apps import Lilya
from lilya.exceptions import PermissionDenied
from lilya.permissions import DefinePermission
from lilya.protocols.permissions import PermissionProtocol
from lilya.requests import Request
from lilya.routing import Include, Path
from lilya.types import ASGIApp, Receive, Scope, Send


class AllowAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-access" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()


class AdminAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-admin" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()


async def home():
    return "Hello world"


async def user(user: str):
    return f"Hello {user}"


# Via Path
app = Lilya(
    routes=[
        Path("/", handler=home),
        Path(
            "/{user}",
            handler=user,
            permissions=[
                DefinePermission(AdminAccess),
            ],
        ),
    ],
    permissions=[DefinePermission(AllowAccess)],
)


# Via Include
app = Lilya(
    routes=[
        Include(
            "/",
            routes=[
                Path("/", handler=home),
                Path(
                    "/{user}",
                    handler=user,
                    permissions=[
                        DefinePermission(AdminAccess),
                    ],
                ),
            ],
            permissions=[DefinePermission(AllowAccess)],
        )
    ]
)

Permissão Pura ASGI

O Lilya segue a especificação ASGI. Isso permite a implementação de permissões ASGI usando a interface ASGI diretamente. Isso envolve a criação de uma cadeia de aplicações ASGI que chamam o seguinte.

Exemplo da abordagem mais comum

from lilya.types import ASGIApp, Scope, Receive, Send


class MinhaPermissao:
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        await self.app(scope, receive, send)

Ao implementar uma permissão pura ASGI, é como implementar uma aplicação ASGI, o primeiro parâmetro deve ser sempre uma aplicação e o método __call__ deve retornar sempre a aplicação.

Permissões e as definições

Uma das vantagens do Lilya é aproveitar as definições para tornar o código-base organizado, limpo e fácil de manter. Conforme mencionado no documento das definições, as permissões são uma das propriedades disponíveis para iniciar uma aplicação Lilya.

from __future__ import annotations

from dataclasses import dataclass

from lilya.conf.global_settings import Settings
from lilya.exceptions import PermissionDenied
from lilya.permissions import DefinePermission
from lilya.protocols.permissions import PermissionProtocol
from lilya.requests import Request
from lilya.types import ASGIApp, Receive, Scope, Send


class AllowAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-access" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()


@dataclass
class AppSettings(Settings):
    @property
    def permissions(self) -> list[DefinePermission]:
        """
        All the permissions to be added when the application starts.
        """
        return [
            DefinePermission(AllowAccess),
        ]