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.