diff --git a/docs/source/developer/captive_portal_mock.rst b/docs/source/developer/captive_portal_mock.rst index 6bf8da38..d7206642 100644 --- a/docs/source/developer/captive_portal_mock.rst +++ b/docs/source/developer/captive_portal_mock.rst @@ -19,10 +19,10 @@ Captive Portal Login Mock View This view looks for ``auth_pass`` or ``password`` in the POST request data, and if it finds anything will try to look for any ``RadiusToken`` instance -having its key equal to this value, and if it does find one, it will create a -radius session (an entry in the ``radacct`` table) related to the user -to which the radius token belongs, provided there's no other open session -for the same user. +having its key equal to this value, and if it does find one, it makes a +``POST`` request to accouting view to create the radius session related to +the user to which the radius token belongs, provided there's no other open +session for the same user. Captive Portal Logout Mock View ------------------------------- @@ -32,6 +32,5 @@ Captive Portal Logout Mock View This view looks for an entry in the ``radacct`` table with ``session_id`` equals to what is passed in the ``logout_id`` POST field and if it finds -one, it flags the session as terminated with ``User-Request`` -termination cause and populates the ``stop_time`` column with the current -UTC time. +one, it makes a ``POST`` request to accounting view to flags the session +as terminated by passing ``User-Request`` as termination cause. diff --git a/openwisp_radius/admin.py b/openwisp_radius/admin.py index 1b673b69..529e1fdf 100644 --- a/openwisp_radius/admin.py +++ b/openwisp_radius/admin.py @@ -666,6 +666,8 @@ class OrganizationRadiusSettingsInline(admin.StackedInline): 'sms_verification', 'sms_sender', 'allowed_mobile_prefixes', + 'login_url', + 'status_url', ) }, ), diff --git a/openwisp_radius/api/views.py b/openwisp_radius/api/views.py index c6a187c0..4c7e46ba 100644 --- a/openwisp_radius/api/views.py +++ b/openwisp_radius/api/views.py @@ -41,7 +41,7 @@ from rest_framework.throttling import BaseThrottle # get_ident method from openwisp_radius.api.serializers import RadiusUserSerializer -from openwisp_users.api.authentication import BearerAuthentication +from openwisp_users.api.authentication import BearerAuthentication, SesameAuthentication from openwisp_users.api.permissions import IsOrganizationManager from openwisp_users.api.views import ChangePasswordView as BasePasswordChangeView @@ -268,7 +268,7 @@ class ObtainAuthTokenView( throttle_scope = 'obtain_auth_token' serializer_class = rest_auth_settings.TokenSerializer auth_serializer_class = AuthTokenSerializer - authentication_classes = [] + authentication_classes = [SesameAuthentication] @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): @@ -281,11 +281,13 @@ def post(self, request, *args, **kwargs): """ Obtain the user radius token required for authentication in APIs. """ - serializer = self.auth_serializer_class( - data=request.data, context={'request': request} - ) - serializer.is_valid(raise_exception=True) - user = self.get_user(serializer, *args, **kwargs) + user = request.user + if user.is_anonymous: + serializer = self.auth_serializer_class( + data=request.data, context={'request': request} + ) + serializer.is_valid(raise_exception=True) + user = self.get_user(serializer, *args, **kwargs) token, _ = UserToken.objects.get_or_create(user=user) self.get_or_create_radius_token(user, self.organization, renew=renew_required) self.update_user_details(user) diff --git a/openwisp_radius/apps.py b/openwisp_radius/apps.py index 9353154e..308b4d75 100644 --- a/openwisp_radius/apps.py +++ b/openwisp_radius/apps.py @@ -14,9 +14,11 @@ create_default_groups_handler, organization_post_save, organization_pre_save, + send_email_on_new_accounting_handler, set_default_group_handler, ) from .registration import register_registration_method +from .signals import radius_accounting_success from .utils import load_model, update_user_related_records @@ -62,6 +64,13 @@ def connect_signals(self): RadiusToken = load_model('RadiusToken') RadiusAccounting = load_model('RadiusAccounting') User = get_user_model() + from openwisp_radius.api.freeradius_views import AccountingView + + radius_accounting_success.connect( + send_email_on_new_accounting_handler, + sender=AccountingView, + dispatch_uid='send_email_on_new_accounting', + ) post_save.connect( create_default_groups_handler, diff --git a/openwisp_radius/base/models.py b/openwisp_radius/base/models.py index 36e8ee90..ce8f5844 100644 --- a/openwisp_radius/base/models.py +++ b/openwisp_radius/base/models.py @@ -162,6 +162,8 @@ _IDENTITY_VERIFICATION_ENABLED_HELP_TEXT = _( 'Whether identity verification is required at the time of user registration' ) +_LOGIN_URL_HELP_TEXT = _('Enter the url where users can log in to the wifi service') +_STATUS_URL_HELP_TEXT = _('Enter the url where users can log out from the wifi service') class AutoUsernameMixin(object): @@ -1128,6 +1130,8 @@ class AbstractOrganizationRadiusSettings(UUIDModel): default=True, help_text=_REGISTRATION_ENABLED_HELP_TEXT, ) + login_url = models.URLField(null=True, blank=True, help_text=_LOGIN_URL_HELP_TEXT) + status_url = models.URLField(null=True, blank=True, help_text=_STATUS_URL_HELP_TEXT) class Meta: verbose_name = _('Organization radius settings') diff --git a/openwisp_radius/locale/de/LC_MESSAGES/django.po b/openwisp_radius/locale/de/LC_MESSAGES/django.po index a776f2cb..72e7146a 100644 --- a/openwisp_radius/locale/de/LC_MESSAGES/django.po +++ b/openwisp_radius/locale/de/LC_MESSAGES/django.po @@ -126,3 +126,15 @@ msgstr "Passwort zurücksetzen" #, python-format msgid "Password reset on %s" msgstr "Passwort zurückgesetzt auf %s" + +#: openwisp_radius/templates/radius_accounting_start.html:3 +msgid "New session has been started for your account with username:" +msgstr "Eine neue Sitzung wurde für Ihr Konto mit dem Benutzernamen gestartet:" + +#: openwisp_radius/templates/radius_accounting_start.html:4 +msgid "You can manage your account or terminate this session any time by clicking on the button below." +msgstr "Sie können Ihr Konto verwalten oder diese Sitzung jederzeit beenden, indem Sie auf die Schaltfläche unten klicken." + +#: openwisp_radius/tasks.py:75 +msgid "Manage Session" +msgstr "Sitzung verwalten" diff --git a/openwisp_radius/locale/fur/LC_MESSAGES/django.po b/openwisp_radius/locale/fur/LC_MESSAGES/django.po index 9fc52d8b..0d4ae96e 100644 --- a/openwisp_radius/locale/fur/LC_MESSAGES/django.po +++ b/openwisp_radius/locale/fur/LC_MESSAGES/django.po @@ -127,3 +127,15 @@ msgstr "Resetta la password" #, python-format msgid "Password reset on %s" msgstr "Reimposta password su %s" + +#: openwisp_radius/templates/radius_accounting_start.html:3 +msgid "New session has been started for your account with username:" +msgstr "È stata avviata una nuova sessione per il tuo account con nome utente:" + +#: openwisp_radius/templates/radius_accounting_start.html:4 +msgid "You can manage your account or terminate this session any time by clicking on the button below." +msgstr "Puoi gestire il tuo account o terminare questa sessione in qualsiasi momento facendo clic sul pulsante in basso." + +#: openwisp_radius/tasks.py:75 +msgid "Manage Session" +msgstr "Gestisci sessione" diff --git a/openwisp_radius/locale/it/LC_MESSAGES/django.po b/openwisp_radius/locale/it/LC_MESSAGES/django.po index c1e33402..b38f4a28 100644 --- a/openwisp_radius/locale/it/LC_MESSAGES/django.po +++ b/openwisp_radius/locale/it/LC_MESSAGES/django.po @@ -125,3 +125,15 @@ msgstr "Resetta la password" #, python-format msgid "Password reset on %s" msgstr "Reimposta password su %s" + +#: openwisp_radius/templates/radius_accounting_start.html:3 +msgid "New session has been started for your account with username:" +msgstr "È stata avviata una nuova sessione per il tuo account con nome utente:" + +#: openwisp_radius/templates/radius_accounting_start.html:4 +msgid "You can manage your account or terminate this session any time by clicking on the button below." +msgstr "Puoi gestire il tuo account o terminare questa sessione in qualsiasi momento facendo clic sul pulsante in basso." + +#: openwisp_radius/tasks.py:75 +msgid "Manage Session" +msgstr "Gestisci sessione" diff --git a/openwisp_radius/locale/ru/LC_MESSAGES/django.po b/openwisp_radius/locale/ru/LC_MESSAGES/django.po index 00dbd889..27edda82 100644 --- a/openwisp_radius/locale/ru/LC_MESSAGES/django.po +++ b/openwisp_radius/locale/ru/LC_MESSAGES/django.po @@ -122,3 +122,15 @@ msgstr "Сброс пароля" #, python-format msgid "Password reset on %s" msgstr "Сброс пароля %s" + +#: openwisp_radius/templates/radius_accounting_start.html:3 +msgid "New session has been started for your account with username:" +msgstr "Новый сеанс запущен для вашей учетной записи с именем пользователя:" + +#: openwisp_radius/templates/radius_accounting_start.html:4 +msgid "Please click on the button below to manage your session:" +msgstr "Вы можете управлять своей учетной записью или прекратить этот сеанс в любое время, нажав кнопку ниже." + +#: openwisp_radius/tasks.py:75 +msgid "Manage Session" +msgstr "Управление сеансом" diff --git a/openwisp_radius/locale/sl/LC_MESSAGES/django.po b/openwisp_radius/locale/sl/LC_MESSAGES/django.po index 38b4f04c..0780b94f 100644 --- a/openwisp_radius/locale/sl/LC_MESSAGES/django.po +++ b/openwisp_radius/locale/sl/LC_MESSAGES/django.po @@ -127,3 +127,15 @@ msgstr "Ponastavitev gesla" #, python-format msgid "Password reset on %s" msgstr "Ponastavitev gesla na %s" + +#: openwisp_radius/templates/radius_accounting_start.html:3 +msgid "New session has been started for your account with username:" +msgstr "Za vaš račun z uporabniškim imenom se je začela nova seja:" + +#: openwisp_radius/templates/radius_accounting_start.html:4 +msgid "You can manage your account or terminate this session any time by clicking on the button below." +msgstr "Svoj račun lahko upravljate ali kadar koli prekinete to sejo s klikom na spodnji gumb." + +#: openwisp_radius/tasks.py:75 +msgid "Manage Session" +msgstr "Upravljanje seje" diff --git a/openwisp_radius/migrations/0025_login_status_url_org_settings.py b/openwisp_radius/migrations/0025_login_status_url_org_settings.py new file mode 100644 index 00000000..c0acbbfe --- /dev/null +++ b/openwisp_radius/migrations/0025_login_status_url_org_settings.py @@ -0,0 +1,31 @@ +# Generated by Django 3.1.8 on 2021-11-19 07:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('openwisp_radius', '0024_registereduser_modified'), + ] + + operations = [ + migrations.AddField( + model_name='organizationradiussettings', + name='login_url', + field=models.URLField( + blank=True, + help_text='Enter the url where users can log in to the wifi service', + null=True, + ), + ), + migrations.AddField( + model_name='organizationradiussettings', + name='status_url', + field=models.URLField( + blank=True, + help_text='Enter the url where users can log out from the wifi service', + null=True, + ), + ), + ] diff --git a/openwisp_radius/receivers.py b/openwisp_radius/receivers.py index b4ce47dd..0a6c02af 100644 --- a/openwisp_radius/receivers.py +++ b/openwisp_radius/receivers.py @@ -1,10 +1,28 @@ """ Receiver functions for django signals (eg: post_save) """ +import logging + +from celery.exceptions import OperationalError + +from openwisp_radius.tasks import send_login_email + from . import settings as app_settings from . import tasks from .utils import create_default_groups, load_model +logger = logging.getLogger(__name__) + + +def send_email_on_new_accounting_handler(sender, accounting_data, **kwargs): + stop_time = accounting_data.get('stop_time', None) + update_time = accounting_data.get('update_time', None) + if stop_time is None and update_time is None: + try: + send_login_email.delay(accounting_data) + except OperationalError: + logger.warn('Celery broker is unreachable') + def set_default_group_handler(sender, instance, created, **kwargs): if created: diff --git a/openwisp_radius/tasks.py b/openwisp_radius/tasks.py index a2bc3832..4bb87c59 100644 --- a/openwisp_radius/tasks.py +++ b/openwisp_radius/tasks.py @@ -1,5 +1,17 @@ +import logging + +import swapper from celery import shared_task +from django.contrib.auth import get_user_model from django.core import management +from django.core.exceptions import ObjectDoesNotExist +from django.template import loader +from django.utils.translation import activate +from django.utils.translation import gettext_lazy as _ + +from openwisp_utils.admin_theme.email import send_email + +logger = logging.getLogger(__name__) @shared_task @@ -39,3 +51,41 @@ def delete_unverified_users(older_than_days=1, exclude_methods=''): @shared_task def convert_called_station_id(unique_id=None): management.call_command('convert_called_station_id', unique_id=unique_id) + + +@shared_task +def send_login_email(accounting_data): + User = get_user_model() + Organization = swapper.load_model('openwisp_users', 'Organization') + username = accounting_data.get('username', None) + org_uuid = accounting_data.get('organization') + try: + user = User.objects.get(username=username) + organization = Organization.objects.get(id=org_uuid) + if organization.is_member(user): + from sesame.utils import get_query_string + + org_radius_settings = organization.radius_settings + login_url = org_radius_settings.login_url + if login_url: + activate(user.language) + one_time_login_url = login_url + get_query_string(user) + subject = _('New radius accounting session started') + context = { + 'user': user, + 'subject': subject, + 'call_to_action_url': one_time_login_url, + 'call_to_action_text': _('Manage Session'), + } + body_html = loader.render_to_string( + 'radius_accounting_start.html', context + ) + send_email(subject, body_html, body_html, [user.email], context) + else: + logger.error( + f'login_url is not defined for {organization.name} organization' + ) + else: + logger.warn(f'{username} is not the member of {organization.name}') + except ObjectDoesNotExist: + logger.warn(f'user with {username} does not exists') diff --git a/openwisp_radius/templates/radius_accounting_start.html b/openwisp_radius/templates/radius_accounting_start.html new file mode 100644 index 00000000..bb54afc4 --- /dev/null +++ b/openwisp_radius/templates/radius_accounting_start.html @@ -0,0 +1,5 @@ +{% load i18n %}{% load l10n %} +
+

{% trans "New session has been started for your account with username:" %} {{ user.get_username }}

+

{% trans "You can manage your account or terminate this session any time by clicking on the button below." %}

+
diff --git a/openwisp_radius/tests/test_api/test_freeradius_api.py b/openwisp_radius/tests/test_api/test_freeradius_api.py index 3252f6ca..66ca51f2 100644 --- a/openwisp_radius/tests/test_api/test_freeradius_api.py +++ b/openwisp_radius/tests/test_api/test_freeradius_api.py @@ -4,6 +4,7 @@ from unittest import mock import swapper +from celery.exceptions import OperationalError from dateutil import parser from django.contrib.auth import get_user_model from django.core.cache import cache @@ -13,6 +14,7 @@ from django.utils.timezone import now from freezegun import freeze_time +from openwisp_radius.api.freeradius_views import AccountingView from openwisp_utils.tests import capture_any_output, catch_signal from ... import registration @@ -678,7 +680,8 @@ def test_accounting_no_token_403(self): ) @freeze_time(START_DATE) - def test_accounting_start_200(self): + @mock.patch('openwisp_radius.receivers.send_login_email.delay') + def test_accounting_start_200(self, send_login_email): self.assertEqual(RadiusAccounting.objects.count(), 0) ra = self._create_radius_accounting(**self._acct_initial_data) data = self._prep_start_acct_data() @@ -693,7 +696,17 @@ def test_accounting_start_200(self): ra.refresh_from_db() self.assertAcctData(ra, data) - def test_accounting_start_radius_token_201(self): + @mock.patch( + 'openwisp_radius.receivers.send_login_email.delay', side_effect=OperationalError + ) + @mock.patch('openwisp_radius.receivers.logger') + def test_celery_broker_unreachable(self, logger, *args): + data = self._prep_start_acct_data() + radius_accounting_success.send(sender=AccountingView, accounting_data=data) + logger.warn.assert_called_with('Celery broker is unreachable') + + @mock.patch('openwisp_radius.receivers.send_login_email.delay') + def test_accounting_start_radius_token_201(self, send_login_email): self._get_org_user() self._login_and_obtain_auth_token() data = self._prep_start_acct_data() @@ -706,6 +719,7 @@ def test_accounting_start_radius_token_201(self): content_type='application/json', ) handler.assert_called_once() + send_login_email.assert_called_once() self.assertEqual(response.status_code, 201) self.assertIsNone(response.data) self.assertEqual(RadiusAccounting.objects.count(), 1) diff --git a/openwisp_radius/tests/test_tasks.py b/openwisp_radius/tests/test_tasks.py index 4adebf69..6b1532e2 100644 --- a/openwisp_radius/tests/test_tasks.py +++ b/openwisp_radius/tests/test_tasks.py @@ -1,9 +1,10 @@ from datetime import timedelta +from unittest import mock from celery import Celery from celery.contrib.testing.worker import start_worker from django.contrib.auth import get_user_model -from django.core import management +from django.core import mail, management from django.utils.timezone import now from openwisp_radius import tasks @@ -104,3 +105,81 @@ def test_delete_unverified_users(self): self.assertEqual(User.objects.count(), 3) tasks.delete_unverified_users.delay(older_than_days=2) self.assertEqual(User.objects.count(), 0) + + @mock.patch('openwisp_radius.tasks.logger') + @mock.patch('openwisp_radius.tasks.activate') + @capture_stdout() + def test_send_login_email(self, translation_activate, logger): + + accounting_data = _RADACCT.copy() + + total_mails = len(mail.outbox) + with self.subTest('do not send email if username is invalid'): + tasks.send_login_email.delay(accounting_data) + self.assertEqual(len(mail.outbox), total_mails) + logger.warn.assert_called_with( + 'user with {} does not exists'.format(accounting_data.get('username')) + ) + + logger.reset_mock() + user = self._get_user() + accounting_data['username'] = user.username + organization = self._get_org() + accounting_data['organization'] = organization.id + + with self.subTest('do not send mail if user is not a member of organization'): + tasks.send_login_email.delay(accounting_data) + self.assertEqual(len(mail.outbox), total_mails) + logger.warn.assert_called_with( + f'{user.username} is not the member of {organization.name}' + ) + translation_activate.assert_not_called() + + logger.reset_mock() + self._create_org_user() + + with self.subTest( + 'do not send mail if login_url does not exists for the organization' + ): + tasks.send_login_email.delay(accounting_data) + self.assertEqual(len(mail.outbox), total_mails) + logger.error.assert_called_with( + f'login_url is not defined for {organization.name} organization' + ) + translation_activate.assert_not_called() + + radius_settings = organization.radius_settings + radius_settings.login_url = 'https://wifi.openwisp.org/default/login/' + radius_settings.save(update_fields=['login_url']) + + with self.subTest( + 'it should send mail if login_url exists for the organization' + ): + tasks.send_login_email.delay(accounting_data) + self.assertEqual(len(mail.outbox), total_mails + 1) + email = mail.outbox.pop() + self.assertRegex( + ''.join(email.alternatives[0][0].splitlines()), + '.*Manage Session.*<\/a>', + ) + self.assertIn( + 'New session has been started for your account with username:' + f' {user.username}', + ' '.join(email.alternatives[0][0].split()), + ) + self.assertIn( + 'You can manage your account or terminate this' + ' session any time by clicking on the button below.', + ' '.join(email.alternatives[0][0].split()), + ) + translation_activate.assert_called_with(user.language) + + with self.subTest('it should send mail in user language preference'): + user.language = 'it' + user.save(update_fields=['language']) + tasks.send_login_email.delay(accounting_data) + self.assertRegex( + ''.join(email.alternatives[0][0].splitlines()), + '.*Manage Session.*<\/a>', + ) + translation_activate.assert_called_with(user.language) diff --git a/setup.py b/setup.py index 247138c4..4f86b551 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,10 @@ 'django>=3.0,<3.2', # Needed for the new authentication backend in openwisp-users # TODO: remove when the new version of openwisp-users is released - 'openwisp-users @ https://github.com/openwisp/openwisp-users/tarball/master', + ( + 'openwisp-users @ https://github.com/openwisp/openwisp-users/' + 'tarball/added-one-time-token' + ), # TODO: change this when next point version of openwisp-utils is released ( 'openwisp-utils[rest] @' diff --git a/tests/openwisp2/integrations/tests.py b/tests/openwisp2/integrations/tests.py index 87768ee8..7999cf53 100644 --- a/tests/openwisp2/integrations/tests.py +++ b/tests/openwisp2/integrations/tests.py @@ -1,5 +1,8 @@ +from unittest import mock +from urllib.parse import urlparse + from django.contrib.auth import get_user_model -from django.test import TestCase +from django.test import Client, TestCase from django.urls import reverse from openwisp_radius.utils import load_model @@ -11,6 +14,13 @@ User = get_user_model() +def mock_post_request(url, data, timeout, *args, **kwargs): + client = Client() + url = urlparse(url).path + assert url == reverse('radius:accounting') + client.post(url, data=data, content_type='application/json') + + class TestIntegrations(TestOrganizationMixin, TestCase): def test_swagger_api_docs(self): admin = self._get_admin() @@ -18,7 +28,17 @@ def test_swagger_api_docs(self): response = self.client.get(reverse('schema-swagger-ui'), {'format': 'openapi'}) self.assertEqual(response.status_code, 200) - def test_captive_portal_login_mock(self): + def _create_rad_token(self): + user = self._get_user() + org = self._get_org() + radius_token = RadiusToken.objects.filter(user=user, organization=org).first() + if not radius_token: + radius_token = RadiusToken.objects.create(user=user, organization=org) + return radius_token + + @capture_any_output() + @mock.patch('openwisp2.views.requests.post', mock_post_request) + def test_captive_portal_login_mock(self, *args, **kwargs): url = reverse('captive_portal_login_mock') radius_token, ra = (None, None) @@ -30,8 +50,7 @@ def test_captive_portal_login_mock(self): with self.subTest('radius token matches, session is created'): user = self._get_user() - org = self._get_org() - radius_token = RadiusToken.objects.create(user=user, organization=org) + radius_token = self._create_rad_token() response = self.client.post(url, {'auth_pass': radius_token.key}) self.assertEqual(response.status_code, 200) self.assertEqual(RadiusToken.objects.count(), 1) @@ -43,7 +62,8 @@ def test_captive_portal_login_mock(self): return radius_token, ra @capture_any_output() - def test_captive_portal_logout_mock(self): + @mock.patch('openwisp2.views.requests.post', mock_post_request) + def test_captive_portal_logout_mock(self, *args, **kwargs): url = reverse('captive_portal_logout_mock') with self.subTest('no action to perform, ensure no error'): @@ -52,8 +72,11 @@ def test_captive_portal_logout_mock(self): response = self.client.post(url, {'logout_id': '123'}) self.assertEqual(response.status_code, 200) - radius_token, ra = self.test_captive_portal_login_mock() - + radius_token = self._create_rad_token() + response = self.client.post( + reverse('captive_portal_login_mock'), {'auth_pass': radius_token.key} + ) + ra = RadiusAccounting.objects.first() with self.subTest('logout_id matches, RadiusAccounting closed'): assert radius_token assert ra diff --git a/tests/openwisp2/sample_radius/apps.py b/tests/openwisp2/sample_radius/apps.py index d165fb4d..faaaea86 100644 --- a/tests/openwisp2/sample_radius/apps.py +++ b/tests/openwisp2/sample_radius/apps.py @@ -1,7 +1,19 @@ from openwisp_radius.apps import OpenwispRadiusConfig +from openwisp_radius.receivers import send_email_on_new_accounting_handler +from openwisp_radius.signals import radius_accounting_success class SampleOpenwispRadiusConfig(OpenwispRadiusConfig): name = 'openwisp2.sample_radius' label = 'sample_radius' verbose_name = 'Sample Radius' + + def connect_signals(self): + from .api.views import AccountingView + + radius_accounting_success.connect( + send_email_on_new_accounting_handler, + sender=AccountingView, + dispatch_uid='send_email_on_new_accounting', + ) + return super().connect_signals() diff --git a/tests/openwisp2/sample_radius/migrations/0025_login_status_url_org_settings.py b/tests/openwisp2/sample_radius/migrations/0025_login_status_url_org_settings.py new file mode 100644 index 00000000..f069dfd6 --- /dev/null +++ b/tests/openwisp2/sample_radius/migrations/0025_login_status_url_org_settings.py @@ -0,0 +1,31 @@ +# Generated by Django 3.1.8 on 2021-11-19 12:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sample_radius', '0023_registereduser_modified'), + ] + + operations = [ + migrations.AddField( + model_name='organizationradiussettings', + name='login_url', + field=models.URLField( + blank=True, + help_text='Enter the url where users can log in to the wifi service', + null=True, + ), + ), + migrations.AddField( + model_name='organizationradiussettings', + name='status_url', + field=models.URLField( + blank=True, + help_text='Enter the url where users can log out from the wifi service', + null=True, + ), + ), + ] diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py index 4fc67abd..289d6e6b 100644 --- a/tests/openwisp2/settings.py +++ b/tests/openwisp2/settings.py @@ -56,6 +56,7 @@ AUTHENTICATION_BACKENDS = ( 'openwisp_users.backends.UsersAuthenticationBackend', 'djangosaml2.backends.Saml2Backend', + 'sesame.backends.ModelBackend', ) AUTH_USER_MODEL = 'openwisp_users.User' @@ -73,6 +74,7 @@ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'sesame.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'djangosaml2.middleware.SamlSessionMiddleware', diff --git a/tests/openwisp2/views.py b/tests/openwisp2/views.py index 0988da1b..cea9e356 100644 --- a/tests/openwisp2/views.py +++ b/tests/openwisp2/views.py @@ -1,9 +1,11 @@ import logging import uuid -from datetime import datetime +from urllib.parse import urlparse +import requests from django.contrib.auth import get_user_model from django.http import HttpResponse +from django.urls import reverse from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt @@ -15,6 +17,18 @@ logger = logging.getLogger('django.server') +def post_accounting_data(request, data): + parsed_url = urlparse(request.build_absolute_uri()) + if parsed_url.netloc and parsed_url.scheme: + url = '{}://{}{}'.format( + parsed_url.scheme, parsed_url.netloc, reverse('radius:accounting') + ) + try: + requests.post(url=url, data=data, timeout=2) + except Exception as err: + logger.warning(err) + + @csrf_exempt @xframe_options_exempt def captive_portal_login(request): @@ -30,7 +44,9 @@ def captive_portal_login(request): ).exists() ): id_ = uuid.uuid4().hex - ra = RadiusAccounting( + username = radius_token.user.username + data = dict( + status_type='Start', username=radius_token.user.username, organization_id=radius_token.organization_id, unique_id=id_, @@ -38,12 +54,12 @@ def captive_portal_login(request): nas_ip_address='127.0.0.1', calling_station_id='00:00:00:00:00:00', called_station_id='11:00:00:00:00:11', + session_time=0, + input_octets=0, + output_octets=0, ) - ra.full_clean() - ra.save() - logger.info( - f'RadiusAccounting session {ra.session_id} created for {ra.username}' - ) + post_accounting_data(request, data) + logger.info(f'RadiusAccounting session {id_} created for {username}') return HttpResponse('logged in') @@ -58,9 +74,15 @@ def captive_portal_logout(request): except RadiusAccounting.DoesNotExist: ra = None if ra: - ra.stop_time = datetime.utcnow() - ra.terminate_cause = 'User-Request' - ra.save() + data = dict( + status_type='Stop', + session_id=session_id, + username=ra.username, + terminate_cause='User-Request', + unique_id=session_id, + nas_ip_address='127.0.0.1', + ) + post_accounting_data(request, data) logger.info( f'RadiusAccounting session {ra.session_id} terminated by {ra.username}' )