diff --git a/docs/index.rst b/docs/index.rst index 3fa79b11..eddcf057 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,6 +44,7 @@ OpenWISP architecture. user/radius_monitoring user/management_commands.rst user/rest-api.rst + user/websocket-api.rst user/settings.rst .. toctree:: diff --git a/docs/user/generating_users.rst b/docs/user/generating_users.rst index 59bd7410..bd6c1898 100644 --- a/docs/user/generating_users.rst +++ b/docs/user/generating_users.rst @@ -84,3 +84,16 @@ REST API: Batch user creation See API documentation: :ref:`Batch user creation `. + +Real-time batch status via WebSocket +------------------------------------ + +When the number of users to generate meets or exceeds +:ref:`OPENWISP_RADIUS_BATCH_ASYNC_THRESHOLD +`, the operation runs +asynchronously via Celery and the batch status is delivered to connected +clients in real time. + +See :ref:`WebSocket API Reference: Batch User Creation Status +` for the endpoint URL, message format, and +integration example. diff --git a/docs/user/importing_users.rst b/docs/user/importing_users.rst index 748cb7ab..de168314 100644 --- a/docs/user/importing_users.rst +++ b/docs/user/importing_users.rst @@ -116,3 +116,16 @@ REST API: Batch user creation See :ref:`API documentation: Batch user creation `. + +Real-time batch status via WebSocket +------------------------------------ + +When the number of users to import meets or exceeds +:ref:`OPENWISP_RADIUS_BATCH_ASYNC_THRESHOLD +`, the operation runs +asynchronously via Celery and the batch status is delivered to connected +clients in real time. + +See :ref:`WebSocket API Reference: Batch User Creation Status +` for the endpoint URL, message format, and +integration example. diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 4df3d1c2..1b15dff6 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -111,6 +111,8 @@ The default encryption format for storing radius check values. A list of disabled encryption formats, by default all formats are enabled in order to keep backward compatibility with legacy systems. +.. _openwisp_radius_batch_async_threshold: + ``OPENWISP_RADIUS_BATCH_ASYNC_THRESHOLD`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -123,6 +125,12 @@ keeps the user interface responsive when creating a large number of users. For batches smaller than the threshold, users will be created immediately (synchronously). +.. note:: + + When batch processing runs asynchronously, the final batch status + (``"completed"`` or ``"failed"``) is delivered to connected clients in + real time via the :ref:`WebSocket API `. + ``OPENWISP_RADIUS_BATCH_DEFAULT_PASSWORD_LENGTH`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user/websocket-api.rst b/docs/user/websocket-api.rst new file mode 100644 index 00000000..4da8b9f0 --- /dev/null +++ b/docs/user/websocket-api.rst @@ -0,0 +1,240 @@ +.. _radius_websocket_api: + +WebSocket API Reference +======================= + +.. contents:: **Table of contents**: + :depth: 2 + :local: + +Overview +-------- + +The WebSocket API provides real-time status updates for batch user +creation operations. + +When a batch is processed asynchronously (i.e., the number of users to +generate or import meets or exceeds +:ref:`OPENWISP_RADIUS_BATCH_ASYNC_THRESHOLD +`), the Django admin interface +automatically connects to the relevant endpoint to receive live status +updates without polling. + +All endpoints: + +- Use JSON messages. +- Require an authenticated staff user (session-based authentication). +- Push real-time updates from the server; no client message is required + after the connection is established. + +Authentication and Authorization +-------------------------------- + +All WebSocket endpoints require an authenticated user. + +A connection is accepted only if the user is authorized to access the +requested resource. The connection is closed immediately if authorization +fails. + +Authentication uses the Django session cookie via ``AuthMiddlewareStack`` +(from ``channels.auth``). DRF token authentication is not supported for +WebSocket connections. + +The ``Origin`` header is validated against ``ALLOWED_HOSTS`` via +``AllowedHostsOriginValidator``. Cross-origin connections from untrusted +hosts are rejected. + +A user is authorized if: + +- The user is a superuser, OR +- The user: + + - Is authenticated and marked as staff, AND + - Is an organization manager for the organization that owns the + requested batch. + +If any check fails, the server closes the connection without sending any +message. + +Connection Endpoints +-------------------- + +1. Batch User Creation Status +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This endpoint delivers real-time status updates for a single batch user +creation operation. + +Connection URL +++++++++++++++ + +:: + + wss:///ws/radius/batch// + +- ````: the hostname and port of the OpenWISP instance. +- ````: the UUID of the ``RadiusBatch`` object to monitor. + +.. note:: + + Use ``wss://`` for HTTPS deployments and ``ws://`` for plain HTTP + (development only). Never use ``ws://`` in production. + +Scope ++++++ + +A single batch user creation operation identified by its UUID. + +Server-Pushed Messages +++++++++++++++++++++++ + +After the connection is established, the client does not need to send any +messages. The server pushes exactly **one** message when batch processing +finishes (either successfully or with an error). + +Message type: ``batch_status_update`` + +.. code-block:: javascript + + { + "status": "" + } + +The ``status`` field contains one of the following values: + +.. list-table:: + :header-rows: 1 + + - - Value + - Description + - - ``"pending"`` + - The batch has been created but processing has not yet started. + This value is not sent via WebSocket; it is visible only through + the REST API or admin interface. + - - ``"processing"`` + - The batch is currently being processed. This value is not sent via + WebSocket; it is the status visible when the admin page is opened + and the WebSocket connection is established. + - - ``"completed"`` + - Batch processing finished successfully. This is a terminal status. + - - ``"failed"`` + - Batch processing encountered an error. This is a terminal status. + +.. note:: + + The server sends exactly one message per connection, always with a + terminal status (``"completed"`` or ``"failed"``). The client should + close the connection after receiving it. + +Connection Lifecycle +++++++++++++++++++++ + +1. The client connects to the endpoint with the batch UUID in the URL. +2. If the user is authorized, the connection is accepted and the client is + added to the channel group ``radius_batch_``. +3. When batch processing finishes, the server sends one + ``batch_status_update`` message containing the terminal status. +4. The client should close the connection upon receiving ``"completed"`` + or ``"failed"``. +5. On disconnect, the client is removed from the channel group. + +Example Client (JavaScript) ++++++++++++++++++++++++++++ + +Example based on the admin interface implementation: + +.. code-block:: javascript + + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = protocol + "//" + window.location.host + + "/ws/radius/batch//"; + const socket = new WebSocket(wsUrl); + + socket.onmessage = function (event) { + const data = JSON.parse(event.data); + if (data.status === "completed" || data.status === "failed") { + socket.close(); + } + }; + + socket.onclose = function (event) { + console.log("RadiusBatch status socket closed."); + }; + +Replace ```` with the UUID of the batch object. + +Deployment Requirements +----------------------- + +WebSocket support requires server-side configuration beyond the default +Django setup. The following components must be in place. + +ASGI Server +~~~~~~~~~~~ + +Django's default WSGI server does not support WebSockets. You must use an +ASGI-compatible server such as `Daphne +`_. + +Install Daphne and add it as the **first entry** in ``INSTALLED_APPS`` so +that Django uses it as the ASGI server: + +.. code-block:: python + + INSTALLED_APPS = [ + "daphne", + # ... other apps + "channels", + # ... + ] + +``ASGI_APPLICATION`` +~~~~~~~~~~~~~~~~~~~~ + +Point Django to your project's ASGI application, which must include the +Channels routing: + +.. code-block:: python + + ASGI_APPLICATION = "your_project.routing.application" + +``CHANNEL_LAYERS`` +~~~~~~~~~~~~~~~~~~ + +A Redis-backed channel layer is required for production deployments. +Install ``channels_redis`` and configure it: + +.. code-block:: python + + CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("localhost", 6379)], + }, + } + } + +WebSocket Routing +~~~~~~~~~~~~~~~~~ + +Import ``openwisp_radius.routing.websocket_urlpatterns`` and include it in +your project's ``URLRouter``. Example ASGI routing module: + +.. code-block:: python + + from channels.auth import AuthMiddlewareStack + from channels.routing import ProtocolTypeRouter, URLRouter + from channels.security.websocket import AllowedHostsOriginValidator + from django.core.asgi import get_asgi_application + + from openwisp_radius.routing import websocket_urlpatterns + + application = ProtocolTypeRouter( + { + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) + ), + "http": get_asgi_application(), + } + )