Source code for ferrosoft.apps.ferrobase.middleware.asgi
from urllib.parse import parse_qs
from channels import middleware
from channels.db import database_sync_to_async
from django.http import HttpResponseForbidden
@database_sync_to_async
def _get_validated_tenant_async(tenant_id: str, user):
from ferrosoft.apps.ferrobase.middleware.tenant import get_validated_tenant
return get_validated_tenant(tenant_id, user)
[docs]
class TenantASGIMiddleware(middleware.BaseMiddleware):
"""
This middleware is like TenantMiddleware but for ASGI applications.
Only query parameters and cookies are inspected for tenant ID.
Well, not exactly the same thing. The resolved tenant is stored in scope field "tenant".
The thread-local variable is *not* written, because it cannot be guaranteed that the ASGI consumer is actually
in the same thread. Thus, consumers need to explicitly use the context manager activate_tenant_database.
"""
async def __call__(self, scope, receive, send):
from ferrosoft.apps.ferrobase.middleware import TenantMiddleware
from ferrosoft.apps.ferrobase.models import User
# Copy scope to stop changes going upstream
scope = dict(scope)
req_key = TenantMiddleware.TENANT_REQUEST_KEY
user = scope.get("user", None)
if not user:
raise ValueError(
"TenantASGIMiddleware cannot find user in scope. "
"AuthMiddleware must be above it."
)
# Anonymous access is not necessary, and only caused by security probings.
if getattr(user, "is_anonymous", False):
return HttpResponseForbidden()
if not isinstance(user, User):
raise ValueError("user is of unexpected type, need ferrobase User instance")
query = parse_qs(scope.get("query_string", None))
if req_key in query:
tenant_id = query[req_key][0]
else:
cookies = scope.get("cookies", {})
tenant_id = cookies.get(req_key, None)
if tenant_id:
scope["tenant"] = await _get_validated_tenant_async(tenant_id, user)
return await super().__call__(scope, receive, send)