Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down
13 changes: 13 additions & 0 deletions docs/user/generating_users.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,16 @@ REST API: Batch user creation

See API documentation: :ref:`Batch user creation
<radius_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
<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
<radius_websocket_api>` for the endpoint URL, message format, and
integration example.
13 changes: 13 additions & 0 deletions docs/user/importing_users.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,16 @@ REST API: Batch user creation

See :ref:`API documentation: Batch user creation
<radius_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
<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
<radius_websocket_api>` for the endpoint URL, message format, and
integration example.
9 changes: 9 additions & 0 deletions docs/user/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -123,6 +125,13 @@ 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
<radius_websocket_api>`.

``OPENWISP_RADIUS_BATCH_DEFAULT_PASSWORD_LENGTH``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
240 changes: 240 additions & 0 deletions docs/user/websocket-api.rst
Original file line number Diff line number Diff line change
@@ -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
<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.
Comment on lines +47 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't document org-scoped access control unless the consumer actually enforces it.

openwisp_radius/consumers.py currently discards the .exists() result in _user_can_access_batch() and then returns True for non-superusers, so this section overstates the authorization guarantee. Please either fix the consumer before merging these docs or soften the wording until batch ownership is really checked.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/user/websocket-api.rst` around lines 47 - 57, The docs state org-scoped
access control for websocket batch access, but openwisp_radius/consumers.py's
_user_can_access_batch() currently discards the QuerySet.exists() result and
ends up returning True for non-superusers, so the code does not actually enforce
batch ownership; fix _user_can_access_batch() to properly evaluate and return
the boolean from the ownership check (ensure the .exists() or equivalent filter
result is returned and used in the authorization decision) or, if you prefer a
docs change instead, soften the wording in docs to remove the assertion that
organization manager ownership is enforced until _user_can_access_batch()
correctly checks and returns the ownership boolean.


Connection Endpoints
---------------------

1. Batch User Creation Status
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This endpoint delivers real-time status updates for a single batch user
creation operation.

Connection URL
++++++++++++++

::

wss://<host>/ws/radius/batch/<batch-id>/

- ``<host>``: the hostname and port of the OpenWISP instance.
- ``<batch-id>``: 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": "<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_<batch-id>``.
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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't promise a terminal message for every accepted connection.

RadiusBatchConsumer only forwards the single group_send emitted from RadiusBatch.process(); it does not send the current batch state on connect. A client that subscribes after that terminal event has already been published will receive zero messages, so the current “exactly one message per connection” wording can leave integrators waiting forever.

Suggested doc fix
-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).
+After the connection is established, the client does not need to send
+any messages. The server pushes at most **one** terminal message
+(``"completed"`` or ``"failed"``) to clients that are already connected
+when the terminal update is emitted.
@@
-    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.
+    The server does not replay the last known status on connect. Clients
+    that subscribe after processing has already finished will not receive
+    a WebSocket message and should fall back to the REST API or a
+    client-side timeout.
@@
-3. When batch processing finishes, the server sends one
-   ``batch_status_update`` message containing the terminal status.
+3. When batch processing finishes, the server sends one
+   ``batch_status_update`` message to clients that are already subscribed.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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": "<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_<batch-id>``.
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.
After the connection is established, the client does not need to send
any messages. The server pushes at most **one** terminal message
(``"completed"`` or ``"failed"``) to clients that are already connected
when the terminal update is emitted.
Message type: ``batch_status_update``
.. code-block:: javascript
{
"status": "<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 does not replay the last known status on connect. Clients
that subscribe after processing has already finished will not receive
a WebSocket message and should fall back to the REST API or a
client-side timeout.
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_<batch-id>``.
3. When batch processing finishes, the server sends one
``batch_status_update`` message to clients that are already subscribed.
4. The client should close the connection upon receiving ``"completed"``
or ``"failed"``.
5. On disconnect, the client is removed from the channel group.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/user/websocket-api.rst` around lines 91 - 139, The docs incorrectly
guarantee "exactly one message per connection"; update the text to state that
RadiusBatchConsumer simply forwards the single group_send emitted by
RadiusBatch.process() and does not send the current batch state on connect, so a
client that subscribes after the terminal group_send has already been published
may receive zero messages; change the wording to clarify that the server will
send at most one terminal batch_status_update (completed/failed) for a given
batch and recommend clients handle the zero-message case (e.g., by polling the
REST API or re-subscribing) instead of relying on a guaranteed message on
connect.


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/<batch-id>/";
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 ``<batch-id>`` 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
<https://github.com/django/daphne>`_.

Install Daphne and add it as the **first entry** in ``INSTALLED_APPS``
so that Django uses it as the ASGI server:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

According to the official Django Channels and Daphne documentation, when and why should daphnebe added as the first entry inINSTALLED_APPS? Is that required for production ASGI deployment, or mainly for Daphne's runserver integration?

💡 Result:

Official docs say to put "daphne" at the start of INSTALLED_APPS when you want Daphne to provide/override Django’s runserver command (i.e., use Daphne as the development server for ASGI), and to ensure it’s ordered before apps like django.contrib.staticfiles that also hook runserver. In Channels 4.x this is explicitly called out because runserver moved into the daphne package and Channels adds a system check to enforce the ordering. [1], [2], [3]

This is not a general requirement for production ASGI deployment. For production you run an ASGI server directly (e.g., daphne myproject.asgi:application), and that does not depend on daphne being first in INSTALLED_APPS; the “put it first” guidance is mainly for Daphne’s runserver integration. [1], [2]

Sources:
[1] Django docs: “How to use Django with Daphne” (runserver integration and “add daphne to the start of INSTALLED_APPS”) (docs.djangoproject.com)
[2] Channels docs: “4.0.0 Release Notes” (runserver moved to daphne; ordering before django.contrib.staticfiles; system check) (channels.readthedocs.io)
[3] Channels tutorial (Daphne in installed apps replaces the standard dev server for runserver) (channels.readthedocs.io)

Citations:


Clarify that daphne in INSTALLED_APPS is for development runserver integration, not production ASGI deployment.

The phrase "so that Django uses it as the ASGI server" is misleading. Per official Django/Channels docs, putting daphne first in INSTALLED_APPS is specifically to override Django's runserver command for development. Production ASGI deployment does not depend on INSTALLED_APPS ordering—you run the server directly (e.g., daphne myproject.asgi:application). The current wording conflates development and production concerns and should clarify the actual purpose.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/user/websocket-api.rst` around lines 175 - 180, Update the wording to
clarify that adding "daphne" as the first entry in INSTALLED_APPS is only to
override Django's runserver for local development (to enable ASGI/WebSocket
support in manage.py runserver), and does not affect production ASGI deployment;
mention that in production you run the ASGI server directly (e.g., daphne
myproject.asgi:application) and that INSTALLED_APPS ordering is not used to
choose the production 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(),
}
)
Loading