Skip to content

Commit

Permalink
[feature] Send email when radius accounting session starts #343
Browse files Browse the repository at this point in the history
Closes #343
  • Loading branch information
codesankalp committed Nov 29, 2021
1 parent 70fbdcf commit a567c53
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 9 deletions.
2 changes: 2 additions & 0 deletions openwisp_radius/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,8 @@ class OrganizationRadiusSettingsInline(admin.StackedInline):
'sms_verification',
'sms_sender',
'allowed_mobile_prefixes',
'login_url',
'status_url',
)
},
),
Expand Down
16 changes: 9 additions & 7 deletions openwisp_radius/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions openwisp_radius/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
create_default_groups_handler,
organization_post_save,
organization_pre_save,
radius_accounting_success_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


Expand Down Expand Up @@ -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(
radius_accounting_success_handler,
sender=AccountingView,
dispatch_uid='radius_accounting_success',
)

post_save.connect(
create_default_groups_handler,
Expand Down
4 changes: 4 additions & 0 deletions openwisp_radius/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Expand Down
31 changes: 31 additions & 0 deletions openwisp_radius/migrations/0025_login_status_url_org_settings.py
Original file line number Diff line number Diff line change
@@ -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,
),
),
]
20 changes: 20 additions & 0 deletions openwisp_radius/receivers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
"""
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 radius_accounting_success_handler(sender, accounting_data, **kwargs):
unique_id = accounting_data.get('unique_id', None)
if unique_id:
RadiusAccounting = load_model('RadiusAccounting')
ra = RadiusAccounting.objects.filter(unique_id=unique_id).first()
if ra and ra.stop_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:
Expand Down
45 changes: 45 additions & 0 deletions openwisp_radius/tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import logging

from celery import shared_task
from django.contrib.auth import get_user_model
from django.core import management
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
Expand Down Expand Up @@ -39,3 +49,38 @@ 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()
username = accounting_data.get('username', None)
org_uuid = accounting_data.get('organization')
user = User.objects.filter(username=username).first()
if user:
organization = (
user.openwisp_users_organization.all().filter(id=org_uuid).first()
)
if organization:
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'
)
9 changes: 9 additions & 0 deletions openwisp_radius/templates/radius_accounting_start.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% load i18n %}{% load l10n %}
<div class="msg">
<p>
{% trans "New session has been started for your account with username:" %} {{ user.get_username }}
</p>
<p>
{% trans "Please click on the button below to manage your session:" %}
</p>
</div>
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
# 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/issue/261-user-preferred-lang'
'openwisp-users @ https://github.com/codesankalp/openwisp-users/'
'tarball/dev'
),
# TODO: change this when next point version of openwisp-utils is released
(
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
),
),
]
2 changes: 2 additions & 0 deletions tests/openwisp2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
AUTHENTICATION_BACKENDS = (
'openwisp_users.backends.UsersAuthenticationBackend',
'djangosaml2.backends.Saml2Backend',
'sesame.backends.ModelBackend',
)

AUTH_USER_MODEL = 'openwisp_users.User'
Expand All @@ -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',
Expand Down

0 comments on commit a567c53

Please sign in to comment.