Permissions¶
Lilya incorporates an inherent permission system designed to facilitate the principle of separation of concerns. Interestingly, this permission system closely resembles middlewares.
In essence, permissions in Lilya function as pure ASGI applications, akin to middlewares, but are specifically tailored to manage access control within an application.
The rationale behind introducing another ASGI-like application, akin to middleware but for permissions, lies in maintaining a clear and singular purpose for each component. Lilya ensures this distinction.
Permissions operate in the sequence after the middleware and before reaching the handler, positioning them ideally for controlling access to the application.
Using the permission¶
The Lilya application class provides a means to include the ASGI permission in a manner that guarantees it remains encapsulated within the 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)],
)
When defining a permission
, it is imperative to utilize lilya.permissions.DefinePermission
to encapsulate it.
Additionally, it is advisable to adhere to the PermissionProtocol
from
lilya.protocols.permissions.PermissionProtocol
as it provides an interface for the definition.
Lilya includes a default exception specifically for denying permissions. Typically, when denying a permission,
a status code 403
is raised along with a specific message. This functionality is encapsulated in
lilya.exceptions.PermissionDenied
.
Furthermore, the details of the message can be customized as needed.
PermissionProtocol¶
For those coming from a more enforced typed language like Java or C#, a protocol is the python equivalent to an interface.
The PermissionProtocol
is simply an interface to build permissions for Lilya by enforcing the implementation of
the __init__
and the async def __call__
.
Enforcing this protocol also aligns with writing a Pure ASGI Permission.
Quick sample¶
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()
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 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)],
)
]
)
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 __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),
]