Skip to content

Lilya Permissions

Use Lilya's Pure ASGI permission system in your Ravyn applications. These protocol-based permissions follow the ASGI specification and can be reused across any ASGI framework for maximum portability and flexibility.

What You'll Learn

  • What Lilya permissions are and how they differ from Ravyn permissions
  • Using the PermissionProtocol
  • Creating Pure ASGI permissions
  • Applying permissions at different application levels
  • Integrating with Ravyn settings
  • When to use Lilya vs Ravyn permissions

Quick Start

from ravyn import Ravyn, get
from lilya.protocols.permissions import PermissionProtocol
from lilya.types import ASGIApp, Scope, Receive, Send
from ravyn.exceptions import NotAuthorized

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

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        # Check if user is admin
        if not scope.get("user", {}).get("is_admin"):
            raise NotAuthorized("Admin access required")
        await self.app(scope, receive, send)

@get("/admin/dashboard")
def admin_dashboard() -> dict:
    return {"data": "Admin dashboard"}

app = Ravyn(
    routes=[...],
    permissions=[AdminOnlyPermission]
)

Important

Do not mix Lilya permissions with Ravyn permissions. Both are independent systems and combining them can cause security issues.


Lilya vs Ravyn Permissions

How to use it

Literally in the same way you would use in Lilya. Yes, that simple!

PermissionProtocol

For those coming from a more enforced typed language like Java or C#, a protocol is the python equivalent to an interface.

from ravyn import Ravyn, Request, Gateway, get
from ravyn.exceptions import PermissionDenied
from lilya.protocols.permissions import PermissionProtocol
from lilya.responses import Ok
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()


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


app = Ravyn(
    routes=[Gateway(handler=user)],
    permissions=[AllowAccess],
)

The PermissionProtocol is simply an interface to build permissions for Ravyn/Lilya by enforcing the implementation of the __init__ and the async def __call__.

Enforcing this protocol also aligns with writing a Pure ASGI Permission.

Permission and the application

Creating this type of permissions will make sure the protocols are followed and therefore reducing development errors by removing common mistakes.

To add middlewares to the application is very simple. You can add it at any level of the application. Those can be included in the Lilya/ChildLilya, Include, Path and WebSocketPath.

from ravyn import Ravyn, Request
from ravyn.exceptions import PermissionDenied
from lilya.protocols.permissions import PermissionProtocol
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 = Ravyn(
    routes=[...],
    permissions=[AllowAccess],
)
from ravyn import Ravyn, Request, Gateway, Include
from ravyn.exceptions import PermissionDenied
from lilya.protocols.permissions import PermissionProtocol
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()


@get()
async def home():
    return "Hello world"


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


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


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

Pure ASGI permission

Lilya follows the ASGI spec. This capability allows for the implementation of ASGI permissions using the ASGI interface directly. This involves creating a chain of ASGI applications that call into the next one.

Example of the most common approach

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

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

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

When implementing a Pure ASGI permission, it is like implementing an ASGI application, the first parameter should always be an app and the __call__ should always return the app.

Permissions and the settings

One of the advantages of Lilya is leveraging the settings to make the codebase tidy, clean and easy to maintain. As mentioned in the settings document, the permissions is one of the properties available to use to start a Lilya application.

from ravyn import RavynSettings, Request
from ravyn.exceptions import PermissionDenied
from lilya.permissions import DefinePermission
from lilya.protocols.permissions import PermissionProtocol
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 AppSettings(RavynSettings):
    @property
    def permissions(self) -> list[DefinePermission]:
        """
        All the permissions to be added when the application starts.
        """
        return [AllowAccess]

Notes

What you should avoid doing?

You cannot mix Lilya permissions with Ravyn permissions. Both are independent and combined can cause security concerns.

Lilya permissions are called on the execution of the __call__ of an ASGI app and Ravyn permissions on a handle_dispatch level.