Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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.
8 changes: 8 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,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 <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
Copy Markdown

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.

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:

.. 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