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.