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." %}
+