django / channels

Developer-friendly asynchrony for Django

Home Page:https://channels.readthedocs.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Middleware support in nested URLRouter or consumers

sourabhv opened this issue · comments

While working on auth with django channels today, I stumbled upon a problem where there is no straight forward way of adding auth for nested router or per-consumer level router

Django rest framework provides a way to do this via a class member of the targetter class/view/consumer in this case. So after reading a few SO questions online and a few parts of codebase, I wrote a middleware something like this, which allows a global and per-consumer level auth classes.

class MultiAuthMiddleware:
    def __init__(
        self,
        app,
        *,
        default_auth_handler: Callable[[dict], bool] = None
    ):
        self.app = app
        self.default_auth_handler = default_auth_handler

    def _get_matching_callback(self, routes, path):
        path = path.lstrip("/")
        for route in routes:
            match = route_pattern_match(route, path)
            if not match:
                continue

            if isinstance(route.callback, URLRouter):
                new_path, args, kwargs = match
                new_routes = route.callback.routes
                return self._get_matching_callback(new_routes, new_path)
            else:
                return route.callback

    def _get_auth_handler_from_callback(self, callback):
        if callback is None:
            return self.default_auth_handler
        return getattr(
            callback.consumer_class,
            'auth_handler',
            self.default_auth_handler
        )

    async def __call__(self, scope, receive, send):
        path = scope['path']
        callback = self._get_matching_callback(self.app.routes, path)
        auth_handler = self._get_auth_handler_from_callback(callback)

        if auth_handler is None:
            # No auth required, so just pass through
            await self.app(scope, receive, send)

        is_authenticated, scope = await auth_handler.authenticate(scope)
        if not is_authenticated:
            # auth failed, deny the connection
            denier = WebsocketDenier()
            return await denier(scope, receive, send)

        # auth succeeded, pass through
        return await self.app(scope, receive, send)

Now this is clearly something I wrote with what I could understand would be the right pattern but I can immediately identify some anti-patterns when compared to rest of your codebase. But this allowed me to solve my problem

So my proposal is to either (in order of preference)

  1. Somehow allow adding middlewares inside each consumer or in any top level or nested URLRouter. Now what I have used as auth_handler is not exactly a middleware (it does not get app instance in init and does not return a new asgi app as callable) but that should be doable
  2. Add this functionality somehow to URLRouter (not sure how that'll work with existing Auth stack middleware)
  3. Add a middleware in package which supports this

Also, if there are some immediate bugs or improvements anyone can see in the above code, do let me know