Skip to content

Lilya Permissions

Now if you are not familiar with Lilya Permisions now it would be a good time to get acquainted.

Historically speaking, Esmerald Permissions came first and offer a different design and feel than the Lilya ones.

Lilya born after Esmerald to be the core and grew to be one of the most powerful frameworks out there, also came with permissions but in the concept of "Pure ASGI Permission".

Relation with Esmerald

Because Esmerald is built on top of Lilya and Lilya does in fact the heavy lifting of the core, it would make sense to provide also the integration with the permissions and the reason for that its because Lilya Pure ASGI Permissions can be reused in Esmerald or any other ASGI framework without any incompatibilities since it follows the ASGI specification.

Now lets get into the good stuff and see how we can use it in Esmerald.

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 esmerald import Esmerald, Request, Gateway, get
from esmerald.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 = Esmerald(
    routes=[Gateway(handler=user)],
    permissions=[AllowAccess],
)

The PermissionProtocol is simply an interface to build permissions for Esmerald/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 esmerald import Esmerald, Request
from esmerald.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 = Esmerald(
    routes=[...],
    permissions=[AllowAccess],
)
from esmerald import Esmerald, Request, Gateway, Include
from esmerald.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 = Esmerald(
    routes=[
        Gateway("/", handler=home),
        Gateway(
            "/{user}",
            handler=user,
            permissions=[AdminAccess],
        ),
    ],
    permissions=[AllowAccess],
)


# Via Include
app = Esmerald(
    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 esmerald import EsmeraldAPISettings, Request
from esmerald.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(EsmeraldAPISettings):
    @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 can mix Lilya permissions with Esmerald permissions but we cannot guarantee that the flows will always work and the reason for that its because those are called in different cirncunstances.

Lilya permissions operate on a Lilya level which is always called before Esmerald due to the fact that its the core but its not always like this.

Usually it would be ok to have cascade permissions between Esmerald and Lilya but if you do have permissions on a Gateway level and HTTPHandler, because both serve different purposes but inherit from Path of Lilya, you will encounter conflicts.

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

Esmerald has unit tests mixing both successfully but the advice you be: Stick with one permission system and be consistent, you do can mix both but you should test to make sure it follows your requirements.