From 4c9a56ca5f4ca379d9c5f19e770ba1204af09e1d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 03:35:05 -0700 Subject: [PATCH 01/11] Add `REACTPY_AUTH_BACKEND` setting --- CHANGELOG.md | 5 ++++ docs/python/configure-asgi-middleware.py | 26 ++++++++++++++++++ docs/python/configure-asgi.py | 6 +---- docs/python/settings.py | 7 +++++ docs/src/get-started/installation.md | 16 ++++++++++- requirements/pkg-deps.txt | 1 + requirements/test-env.txt | 1 - src/reactpy_django/config.py | 5 ++++ src/reactpy_django/websocket/consumer.py | 34 +++++++++++++++++++----- 9 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 docs/python/configure-asgi-middleware.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 06f30dff..e680dbd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,11 @@ Using the following categories, list your changes in this order: ### Added - Added warning if poor system/cache/database performance is detected. +- Added `REACTPY_AUTH_BACKEND` setting to allow for custom authentication backends. + +### Changed + +- Using `AuthMiddlewareStack` is now optional. ## [3.1.0] - 2023-05-06 diff --git a/docs/python/configure-asgi-middleware.py b/docs/python/configure-asgi-middleware.py new file mode 100644 index 00000000..11a4ab88 --- /dev/null +++ b/docs/python/configure-asgi-middleware.py @@ -0,0 +1,26 @@ +# Broken load order, only for type checking +from channels.routing import ProtocolTypeRouter, URLRouter + +from reactpy_django import REACTPY_WEBSOCKET_PATH + + +django_asgi_app = "" + + +# start +from channels.auth import AuthMiddlewareStack # noqa: E402 +from channels.sessions import SessionMiddlewareStack # noqa: E402 + + +application = ProtocolTypeRouter( + { + "http": django_asgi_app, + "websocket": SessionMiddlewareStack( + AuthMiddlewareStack( + URLRouter( + [REACTPY_WEBSOCKET_PATH], + ) + ) + ), + } +) diff --git a/docs/python/configure-asgi.py b/docs/python/configure-asgi.py index 88b37b1e..6ae7cf8a 100644 --- a/docs/python/configure-asgi.py +++ b/docs/python/configure-asgi.py @@ -10,9 +10,7 @@ django_asgi_app = get_asgi_application() -from channels.auth import AuthMiddlewareStack # noqa: E402 from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 -from channels.sessions import SessionMiddlewareStack # noqa: E402 from reactpy_django import REACTPY_WEBSOCKET_PATH # noqa: E402 @@ -20,8 +18,6 @@ application = ProtocolTypeRouter( { "http": django_asgi_app, - "websocket": SessionMiddlewareStack( - AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_PATH])) - ), + "websocket": URLRouter([REACTPY_WEBSOCKET_PATH]), } ) diff --git a/docs/python/settings.py b/docs/python/settings.py index c3c2b0d9..9633da43 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -13,3 +13,10 @@ # Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function, or `None` REACTPY_DEFAULT_QUERY_POSTPROCESSOR = "reactpy_django.utils.django_query_postprocessor" + +# Dotted path to the Django authentication backend to use for ReactPy components +# This is only needed if: +# 1. You are using `AuthMiddlewareStack` and... +# 2. You are using Django's `AUTHENTICATION_BACKENDS` settings and... +# 3. Your Django user model does not define a `backend` attribute +REACTPY_AUTH_BACKEND = None diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md index 21e26201..07be54a3 100644 --- a/docs/src/get-started/installation.md +++ b/docs/src/get-started/installation.md @@ -42,7 +42,7 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt Below are a handful of values you can change within `settings.py` to modify the behavior of ReactPy. - ```python + ```python linenums="0" {% include "../../python/settings.py" %} ``` @@ -66,6 +66,20 @@ Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`. {% include "../../python/configure-asgi.py" %} ``` +??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Optional)" + + If you will need to... + + 1. Access the currently active user + 2. Login or logout users from your components + 3. Access Django's `Sesssion` object + + ... then you will need to ensure your `REACTPY_WEBSOCKET_PATH` is wrapped with `AuthMiddlewareStack` and `SessionMiddlewareStack`. + + ```python linenums="0" + {% include "../../python/configure-asgi-middleware.py" start="# start" %} + ``` + ??? question "Where is my `asgi.py`?" If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html). diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index 16b18367..75758695 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,4 +1,5 @@ channels >=4.0.0 +django >=4.1.0 reactpy >=1.0.0, <1.1.0 aiofile >=3.0 dill >=0.3.5 diff --git a/requirements/test-env.txt b/requirements/test-env.txt index f7552f1d..cd40cf23 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -1,4 +1,3 @@ -django playwright twisted channels[daphne]>=4.0.0 diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index 33950b1e..960aa58f 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -50,3 +50,8 @@ ) ) ) +REACTPY_AUTH_BACKEND: str | None = getattr( + settings, + "REACTPY_AUTH_BACKEND", + None, +) diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index aab994d3..e52ed936 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -27,25 +27,43 @@ class ReactpyAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer): """Communicates with the browser to perform actions on-demand.""" async def connect(self) -> None: - from django.contrib.auth.models import AbstractBaseUser - + """The user has connected.""" await super().connect() - user: AbstractBaseUser = self.scope.get("user") + # Authenticate the user, if possible + from reactpy_django.config import REACTPY_AUTH_BACKEND + + user: Any = self.scope.get("user") if user and user.is_authenticated: try: - await login(self.scope, user) - await database_sync_to_async(self.scope["session"].save)() + await login(self.scope, user, backend=REACTPY_AUTH_BACKEND) except Exception: _logger.exception("ReactPy websocket authentication has failed!") elif user is None: - _logger.warning("ReactPy websocket is missing AuthMiddlewareStack!") + _logger.debug( + "ReactPy websocket is missing AuthMiddlewareStack! " + "Users will not be accessible within `use_scope` or `use_websocket`!" + ) + + # Save the session, if possible + if self.scope.get("session"): + try: + await database_sync_to_async(self.scope["session"].save)() + except Exception: + _logger.exception("ReactPy websocket has failed to save the session!") + else: + _logger.debug( + "ReactPy websocket is missing SessionMiddlewareStack! " + "Sessions will not be accessible within `use_scope` or `use_websocket`!" + ) + # Start allowing component renders self._reactpy_dispatcher_future = asyncio.ensure_future( self._run_dispatch_loop() ) async def disconnect(self, code: int) -> None: + """The user has disconnected.""" if self._reactpy_dispatcher_future.done(): await self._reactpy_dispatcher_future else: @@ -53,9 +71,11 @@ async def disconnect(self, code: int) -> None: await super().disconnect(code) async def receive_json(self, content: Any, **_) -> None: + """Receive a message from the browser. Typically messages are event signals.""" await self._reactpy_recv_queue.put(content) async def _run_dispatch_loop(self): + """Runs the main loop that performs component rendering tasks.""" from reactpy_django import models from reactpy_django.config import ( REACTPY_DATABASE, @@ -130,7 +150,7 @@ async def _run_dispatch_loop(self): ) return - # Begin serving the ReactPy component + # Start the ReactPy component rendering loop try: await serve_layout( Layout(ConnectionContext(component_instance, value=connection)), From 5c88792d19f43f1c933fc919b5295216a8890a6c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 03:43:55 -0700 Subject: [PATCH 02/11] Add tests --- tests/test_app/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index aa2c5196..15d6d1a1 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -170,3 +170,7 @@ }, }, } + + +# ReactPy Django Settings +REACTPY_AUTH_BACKEND = "django.contrib.auth.backends.ModelBackend" From f3616fbed6d1dc90c20ddf60a6771c3f4c1e99fb Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 03:49:26 -0700 Subject: [PATCH 03/11] update docs --- docs/src/get-started/installation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md index 07be54a3..bbe50bf7 100644 --- a/docs/src/get-started/installation.md +++ b/docs/src/get-started/installation.md @@ -66,15 +66,15 @@ Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`. {% include "../../python/configure-asgi.py" %} ``` -??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Optional)" +??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Recommended)" - If you will need to... + There are many situations where you need to access the Django `User` or `Session` objects within ReactPy components. For example, if you want to: 1. Access the currently active user 2. Login or logout users from your components 3. Access Django's `Sesssion` object - ... then you will need to ensure your `REACTPY_WEBSOCKET_PATH` is wrapped with `AuthMiddlewareStack` and `SessionMiddlewareStack`. + In these situations will need to ensure your `REACTPY_WEBSOCKET_PATH` is wrapped with `AuthMiddlewareStack` and/or `SessionMiddlewareStack`. ```python linenums="0" {% include "../../python/configure-asgi-middleware.py" start="# start" %} From 0b0899fbef3f14d65dcabb37c603d3286e587fa5 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 03:50:06 -0700 Subject: [PATCH 04/11] update comment --- docs/python/configure-asgi-middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/python/configure-asgi-middleware.py b/docs/python/configure-asgi-middleware.py index 11a4ab88..0ee33a8a 100644 --- a/docs/python/configure-asgi-middleware.py +++ b/docs/python/configure-asgi-middleware.py @@ -1,4 +1,4 @@ -# Broken load order, only for type checking +# Broken load order, only used for linting from channels.routing import ProtocolTypeRouter, URLRouter from reactpy_django import REACTPY_WEBSOCKET_PATH From 5cf85afa27ddd548173708aa7447166e9fdb2eda Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 03:51:20 -0700 Subject: [PATCH 05/11] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e680dbd0..be5f1982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Using the following categories, list your changes in this order: ### Changed +- Using `SessionMiddlewareStack` is now optional. - Using `AuthMiddlewareStack` is now optional. ## [3.1.0] - 2023-05-06 From 187e431df410905f945463c84211f86b0c3ebee6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 04:06:45 -0700 Subject: [PATCH 06/11] misc docs --- docs/src/features/decorators.md | 2 +- docs/src/get-started/installation.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/features/decorators.md b/docs/src/features/decorators.md index 95779fae..45548799 100644 --- a/docs/src/features/decorators.md +++ b/docs/src/features/decorators.md @@ -8,7 +8,7 @@ You can limit access to a component to users with a specific `auth_attribute` by using this decorator (with or without parentheses). -By default, this decorator checks if the user is logged in, and his/her account has not been deactivated. +By default, this decorator checks if the user is logged in and not deactivated (`is_active`). This decorator is commonly used to selectively render a component only if a user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md index bbe50bf7..e6f49488 100644 --- a/docs/src/get-started/installation.md +++ b/docs/src/get-started/installation.md @@ -66,12 +66,12 @@ Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`. {% include "../../python/configure-asgi.py" %} ``` -??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Recommended)" +??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Optional)" There are many situations where you need to access the Django `User` or `Session` objects within ReactPy components. For example, if you want to: - 1. Access the currently active user - 2. Login or logout users from your components + 1. Access the `User` that is currently logged in + 2. Login or logout the current `User` 3. Access Django's `Sesssion` object In these situations will need to ensure your `REACTPY_WEBSOCKET_PATH` is wrapped with `AuthMiddlewareStack` and/or `SessionMiddlewareStack`. From 3650d05d8efc320c57a901538d1b3980d6fce589 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 04:15:44 -0700 Subject: [PATCH 07/11] misc docs --- docs/src/get-started/installation.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md index e6f49488..daa296f2 100644 --- a/docs/src/get-started/installation.md +++ b/docs/src/get-started/installation.md @@ -28,9 +28,11 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt ReactPy-Django requires ASGI Websockets from [Django Channels](https://github.com/django/channels). - If you have not enabled ASGI on your **Django project** yet, you will need to install `channels[daphne]`, add `daphne` to `INSTALLED_APPS`, then set your `ASGI_APPLICATION` variable. + If you have not enabled ASGI on your **Django project** yet, you will need to - Read the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info. + 1. Install `channels[daphne]` + 2. Add `daphne` to `INSTALLED_APPS` + 3. Set your `ASGI_APPLICATION` variable. === "settings.py" @@ -38,6 +40,8 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt {% include "../../python/configure-channels.py" %} ``` + Consider reading the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info. + ??? note "Configure ReactPy settings (Optional)" Below are a handful of values you can change within `settings.py` to modify the behavior of ReactPy. From 52657941a0909a52f8c5d461cd5d57d301bd0c0e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 04:47:45 -0700 Subject: [PATCH 08/11] misc verbiage --- docs/src/get-started/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md index daa296f2..7aa2ea65 100644 --- a/docs/src/get-started/installation.md +++ b/docs/src/get-started/installation.md @@ -78,7 +78,7 @@ Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`. 2. Login or logout the current `User` 3. Access Django's `Sesssion` object - In these situations will need to ensure your `REACTPY_WEBSOCKET_PATH` is wrapped with `AuthMiddlewareStack` and/or `SessionMiddlewareStack`. + In these situations will need to ensure you are using `AuthMiddlewareStack` and/or `SessionMiddlewareStack`. ```python linenums="0" {% include "../../python/configure-asgi-middleware.py" start="# start" %} From 21e2ffedfdd9e5ca2898e110d4ff3a4483acd8c4 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Jun 2023 04:50:14 -0700 Subject: [PATCH 09/11] update logging message --- src/reactpy_django/websocket/consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index e52ed936..de58e623 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -50,7 +50,7 @@ async def connect(self) -> None: try: await database_sync_to_async(self.scope["session"].save)() except Exception: - _logger.exception("ReactPy websocket has failed to save the session!") + _logger.exception("ReactPy has failed to save scope['session']!") else: _logger.debug( "ReactPy websocket is missing SessionMiddlewareStack! " From 8d112d97fa08cb9dbab34bd13ce1e09a29598f32 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 17 Jun 2023 16:22:31 -0700 Subject: [PATCH 10/11] user -> browser --- src/reactpy_django/websocket/consumer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index de58e623..4a0449e7 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -19,7 +19,6 @@ from reactpy_django.types import ComponentParamData, ComponentWebsocket from reactpy_django.utils import db_cleanup, func_has_params - _logger = logging.getLogger(__name__) @@ -27,7 +26,7 @@ class ReactpyAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer): """Communicates with the browser to perform actions on-demand.""" async def connect(self) -> None: - """The user has connected.""" + """The browser has connected.""" await super().connect() # Authenticate the user, if possible @@ -63,7 +62,7 @@ async def connect(self) -> None: ) async def disconnect(self, code: int) -> None: - """The user has disconnected.""" + """The browser has disconnected.""" if self._reactpy_dispatcher_future.done(): await self._reactpy_dispatcher_future else: From bded29ba01428019b348154c9bf89015e36677f9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 18 Jun 2023 01:03:43 -0700 Subject: [PATCH 11/11] isort --- src/reactpy_django/websocket/consumer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 4a0449e7..de8ec423 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -19,6 +19,7 @@ from reactpy_django.types import ComponentParamData, ComponentWebsocket from reactpy_django.utils import db_cleanup, func_has_params + _logger = logging.getLogger(__name__)