From 3be9af0ea0f6390a1a7af705ce8d6ca0b071f2d7 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 18 Aug 2023 19:11:47 +0530 Subject: [PATCH 01/51] [feature] Added intergraion with openwisp-monitoring - Added timeseries metrics for user-signups and RADIUS traffic --- docs/user/settings.rst | 4 +- openwisp_radius/api/urls.py | 5 + openwisp_radius/api/views.py | 27 +- openwisp_radius/integrations/__init__.py | 0 .../integrations/monitoring/__init__.py | 0 .../integrations/monitoring/admin.py | 40 +++ .../integrations/monitoring/apps.py | 54 ++++ .../integrations/monitoring/configuration.py | 230 ++++++++++++++++++ .../monitoring/migrations/0001_initial.py | 20 ++ .../monitoring/migrations/__init__.py | 42 ++++ .../integrations/monitoring/receivers.py | 38 +++ .../radius-monitoring/css/device-change.css | 14 ++ .../radius-monitoring/js/device-change.js | 82 +++++++ .../integrations/monitoring/tasks.py | 148 +++++++++++ .../radius-monitoring/device/change_form.html | 38 +++ openwisp_radius/registration.py | 4 +- openwisp_radius/tests/test_api/test_api.py | 93 +++++++ setup.cfg | 2 +- tests/openwisp2/routing.py | 12 + tests/openwisp2/settings.py | 99 ++++++-- tests/openwisp2/urls.py | 7 + tests/openwisp2/views.py | 6 +- 22 files changed, 937 insertions(+), 28 deletions(-) create mode 100644 openwisp_radius/integrations/__init__.py create mode 100644 openwisp_radius/integrations/monitoring/__init__.py create mode 100644 openwisp_radius/integrations/monitoring/admin.py create mode 100644 openwisp_radius/integrations/monitoring/apps.py create mode 100644 openwisp_radius/integrations/monitoring/configuration.py create mode 100644 openwisp_radius/integrations/monitoring/migrations/0001_initial.py create mode 100644 openwisp_radius/integrations/monitoring/migrations/__init__.py create mode 100644 openwisp_radius/integrations/monitoring/receivers.py create mode 100644 openwisp_radius/integrations/monitoring/static/radius-monitoring/css/device-change.css create mode 100644 openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js create mode 100644 openwisp_radius/integrations/monitoring/tasks.py create mode 100644 openwisp_radius/integrations/monitoring/templates/admin/config/radius-monitoring/device/change_form.html create mode 100644 tests/openwisp2/routing.py diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 82f7db0e..ee03f87a 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -667,8 +667,8 @@ If this is enabled, each registered user should be verified using a verification method. The following choices are available by default: - ``''`` (empty string): unspecified -- ``manual``: manually created -- ``email``: Email (No Identity Verification) +- ``manual``: Manually created +- ``email``: Email - ``mobile_phone``: Mobile phone number :ref:`verification via SMS ` - ``social_login``: :doc:`social login feature ` diff --git a/openwisp_radius/api/urls.py b/openwisp_radius/api/urls.py index fed1d5ad..193224f0 100644 --- a/openwisp_radius/api/urls.py +++ b/openwisp_radius/api/urls.py @@ -83,6 +83,11 @@ def get_api_urls(api_views=None): api_views.download_rad_batch_pdf, name='download_rad_batch_pdf', ), + path( + 'radius/sessions/', + api_views.radius_accounting, + name='radius_accounting_list', + ), ] else: return [] diff --git a/openwisp_radius/api/views.py b/openwisp_radius/api/views.py index f59dac11..e779707d 100644 --- a/openwisp_radius/api/views.py +++ b/openwisp_radius/api/views.py @@ -18,7 +18,7 @@ from django.utils.translation import gettext_lazy as _ from django.utils.translation.trans_real import get_language_from_request from django.views.decorators.csrf import csrf_exempt -from django_filters.rest_framework import DjangoFilterBackend +from django_filters.rest_framework import CharFilter, DjangoFilterBackend from drf_yasg.utils import no_body, swagger_auto_schema from rest_framework import serializers, status from rest_framework.authentication import SessionAuthentication @@ -41,6 +41,7 @@ from openwisp_radius.api.serializers import RadiusUserSerializer from openwisp_users.api.authentication import BearerAuthentication, SesameAuthentication +from openwisp_users.api.mixins import FilterByOrganizationManaged, ProtectedAPIMixin from openwisp_users.api.permissions import IsOrganizationManager from openwisp_users.api.views import ChangePasswordView as BasePasswordChangeView from openwisp_users.backends import UsersAuthenticationBackend @@ -801,3 +802,27 @@ def create_phone_token(self, *args, **kwargs): change_phone_number = ChangePhoneNumberView.as_view() + + +class RadiusAccountingFilter(AccountingFilter): + called_station_id = CharFilter(field_name='called_station_id', lookup_expr='iexact') + + +@method_decorator( + name='get', + decorator=swagger_auto_schema( + operation_description=""" + Returns all RADIUS sessions of user managed organizations. + """, + ), +) +class RadiusAccountingView(ProtectedAPIMixin, FilterByOrganizationManaged, ListAPIView): + throttle_scrope = 'radius_accounting_list' + serializer_class = RadiusAccountingSerializer + pagination_class = AccountingViewPagination + filter_backends = (DjangoFilterBackend,) + filterset_class = RadiusAccountingFilter + queryset = RadiusAccounting.objects.all().order_by('-start_time') + + +radius_accounting = RadiusAccountingView.as_view() diff --git a/openwisp_radius/integrations/__init__.py b/openwisp_radius/integrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openwisp_radius/integrations/monitoring/__init__.py b/openwisp_radius/integrations/monitoring/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openwisp_radius/integrations/monitoring/admin.py b/openwisp_radius/integrations/monitoring/admin.py new file mode 100644 index 00000000..e859e26c --- /dev/null +++ b/openwisp_radius/integrations/monitoring/admin.py @@ -0,0 +1,40 @@ +from django.contrib import admin +from django.urls import reverse +from swapper import load_model + +Device = load_model('config', 'Device') +RadiusAccounting = load_model('openwisp_radius', 'RadiusAccounting') + +BaseDeviceAdmin = admin.site._registry[Device].__class__ + + +class DeviceAdmin(BaseDeviceAdmin): + change_form_template = 'admin/config/radius-monitoring/device/change_form.html' + + class Media: + js = tuple(BaseDeviceAdmin.Media.js) + ( + 'radius-monitoring/js/device-change.js', + ) + css = { + 'all': ('radius-monitoring/css/device-change.css',) + + BaseDeviceAdmin.Media.css['all'] + } + + def get_extra_context(self, pk=None): + ctx = super().get_extra_context(pk) + ctx.update( + { + 'radius_accounting_api_endpoint': reverse( + 'radius:radius_accounting_list' + ), + 'radius_accounting': reverse( + f'admin:{RadiusAccounting._meta.app_label}' + f'_{RadiusAccounting._meta.model_name}_changelist' + ), + } + ) + return ctx + + +admin.site.unregister(Device) +admin.site.register(Device, DeviceAdmin) diff --git a/openwisp_radius/integrations/monitoring/apps.py b/openwisp_radius/integrations/monitoring/apps.py new file mode 100644 index 00000000..9a2fe125 --- /dev/null +++ b/openwisp_radius/integrations/monitoring/apps.py @@ -0,0 +1,54 @@ +from django.apps import AppConfig +from django.db.models.signals import post_save +from django.utils.translation import gettext_lazy as _ +from openwisp_monitoring.monitoring.configuration import ( + _register_chart_configuration_choice, + register_metric, +) +from swapper import load_model + + +class OpenwispRadiusMonitoringConfig(AppConfig): + name = 'openwisp_radius.integrations.monitoring' + label = 'openwisp_radius_monitoring' + verbose_name = _('OpenWISP RADIUS Monitoring') + + def ready(self): + super().ready() + self.register_radius_metrics() + self.connect_signal_receivers() + + def register_radius_metrics(self): + from .configuration import RADIUS_METRICS + + for metric_key, metric_config in RADIUS_METRICS.items(): + register_metric(metric_key, metric_config) + for chart_key, chart_config in metric_config.get('charts', {}).items(): + _register_chart_configuration_choice(chart_key, chart_config) + + def connect_signal_receivers(self): + from .receivers import ( + post_save_organizationuser, + post_save_radiusaccounting, + post_save_registereduser, + ) + + OrganizationUser = load_model('openwisp_users', 'OrganizationUser') + RegisteredUser = load_model('openwisp_radius', 'RegisteredUser') + RadiusAccounting = load_model('openwisp_radius', 'RadiusAccounting') + + post_save.connect( + post_save_organizationuser, + sender=OrganizationUser, + dispatch_uid='post_save_organizationuser_user_signup_metric', + ) + post_save.connect( + post_save_registereduser, + sender=RegisteredUser, + dispatch_uid='post_save_registereduser_user_signup_metric', + ) + post_save.connect( + post_save_radiusaccounting, + sender=RadiusAccounting, + dispatch_uid='post_save_radiusaccounting_radius_acc_metric', + ) diff --git a/openwisp_radius/integrations/monitoring/configuration.py b/openwisp_radius/integrations/monitoring/configuration.py new file mode 100644 index 00000000..46da43f7 --- /dev/null +++ b/openwisp_radius/integrations/monitoring/configuration.py @@ -0,0 +1,230 @@ +from copy import deepcopy + +from django.utils.translation import gettext_lazy as _ +from openwisp_monitoring.monitoring.configuration import DEFAULT_COLORS + +from openwisp_radius.registration import REGISTRATION_METHOD_CHOICES + +user_signups_chart_traces = {'total': 'lines'} +user_signups_chart_order = ['total'] +user_signups_chart_summary_labels = [_('Total new users')] + +for (method, label) in REGISTRATION_METHOD_CHOICES: + if method == '': + method = 'unspecified' + user_signups_chart_traces[method] = 'stackedbar' + user_signups_chart_summary_labels.append( + _('New %(label)s users' % {"label": label}) + ) + user_signups_chart_order.append(method) + + +user_singups_chart_config = { + 'type': 'stackedbar+lines', + 'trace_type': user_signups_chart_traces, + 'trace_order': user_signups_chart_order, + 'title': _('User Registration'), + 'label': _('User Registration'), + 'description': _('Daily user registration grouped by registration method'), + 'summary_labels': user_signups_chart_summary_labels, + 'order': 240, + 'unit': '', + 'calculate_total': True, + 'query': { + 'influxdb': ( + "SELECT COUNT(DISTINCT(user_id)) FROM " + " {key} WHERE time >= '{time}' {end_date} {organization_id}" + " GROUP BY time(1d), method" + ) + }, + 'query_default_param': { + 'organization_id': '', + }, + 'colors': [ + DEFAULT_COLORS[7], + DEFAULT_COLORS[0], + DEFAULT_COLORS[1], + DEFAULT_COLORS[2], + DEFAULT_COLORS[3], + DEFAULT_COLORS[4], + DEFAULT_COLORS[5], + ], +} + +cumulative_user_singups_chart_config = deepcopy(user_singups_chart_config) +cumulative_user_singups_chart_config['query']['influxdb'] = ( + "SELECT CUMULATIVE_SUM(COUNT(DISTINCT(user_id))) FROM " + " {key} WHERE time >= '{time}' {end_date} {organization_id}" + " GROUP BY time(1d), method" +) +cumulative_user_singups_chart_config['summary_query'] = { + 'influxdb': user_singups_chart_config['query']['influxdb'] +} +cumulative_user_singups_chart_config['title'] = _('Total Registered Users') +cumulative_user_singups_chart_config['label'] = _('Total Registered Users') +cumulative_user_singups_chart_config['order'] = 241 + + +RADIUS_METRICS = { + 'user_signups': { + 'label': _('User Registration'), + 'name': 'User Registration', + 'key': 'user_signups', + 'field_name': 'user_id', + 'charts': { + 'user_signups': user_singups_chart_config, + 'tot_user_signups': cumulative_user_singups_chart_config, + }, + }, + 'radius_acc': { + 'label': _('RADIUS Accounting'), + 'name': '{name}', + 'key': 'radius_acc', + 'field_name': 'input_octets', + 'related_fields': ['output_octets', 'username'], + 'charts': { + 'radius_traffic': { + 'type': 'stackedbar+lines', + 'calculate_total': True, + 'trace_type': { + 'download': 'stackedbar', + 'upload': 'stackedbar', + 'total': 'lines', + }, + 'trace_order': ['total', 'download', 'upload'], + 'title': _('RADIUS Sessions Traffic'), + 'label': _('RADIUS Traffic'), + 'description': _( + 'RADIUS Network traffic (total, download and upload).' + ), + 'summary_labels': [ + _('Total traffic'), + _('Total download traffic'), + _('Total upload traffic'), + ], + 'unit': 'adaptive_prefix+B', + 'order': 241, + 'query': { + 'influxdb': ( + "SELECT SUM(output_octets) / 1000000000 AS upload, " + "SUM(input_octets) / 1000000000 AS download FROM {key} " + "WHERE time >= '{time}' {end_date} " + "AND content_type = '{content_type}' " + "AND object_id = '{object_id}' " + "GROUP BY time(1d)" + ) + }, + 'colors': [ + DEFAULT_COLORS[7], + DEFAULT_COLORS[0], + DEFAULT_COLORS[1], + ], + }, + 'rad_session': { + 'type': 'stackedbar+lines', + 'calculate_total': True, + 'fill': 'none', + 'trace_type': user_signups_chart_traces, + 'trace_order': user_signups_chart_order, + 'title': _('Unique RADIUS Session Count'), + 'label': _('RADIUS Session Count'), + 'description': _( + 'RADIUS Network traffic (total, download and upload).' + ), + 'summary_labels': user_signups_chart_summary_labels, + 'unit': '', + 'order': 242, + 'query': { + 'influxdb': ( + "SELECT COUNT(DISTINCT(username)) FROM {key} " + "WHERE time >= '{time}' {end_date} " + "AND content_type = '{content_type}' " + "AND object_id = '{object_id}' " + "GROUP by time(1d), method" + ) + }, + 'query_default_param': { + 'organization_id': '', + 'location_id': '', + }, + 'colors': user_singups_chart_config['colors'], + }, + }, + }, + 'gen_radius_acc': { + 'label': _('General RADIUS Accounting'), + 'name': 'General RADIUS Accounting', + 'key': 'radius_acc', + 'field_name': 'input_octets', + 'related_fields': ['output_octets'], + 'charts': { + 'gen_rad_traffic': { + 'type': 'stackedbar+lines', + 'calculate_total': True, + 'fill': 'none', + 'trace_type': { + 'download': 'stackedbar', + 'upload': 'stackedbar', + 'total': 'lines', + }, + 'trace_order': ['total', 'download', 'upload'], + 'title': _('Total RADIUS Sessions Traffic'), + 'label': _('General RADIUS Traffic'), + 'description': _( + 'RADIUS Network traffic (total, download and upload).' + ), + 'summary_labels': [ + _('Total traffic'), + _('Total download traffic'), + _('Total upload traffic'), + ], + 'unit': 'adaptive_prefix+B', + 'order': 242, + 'query': { + 'influxdb': ( + "SELECT SUM(output_octets) / 1000000000 AS upload, " + "SUM(input_octets) / 1000000000 AS download FROM {key} " + "WHERE time >= '{time}' {end_date} {organization_id} " + "{location_id} GROUP BY time(1d)" + ) + }, + 'query_default_param': { + 'organization_id': '', + 'location_id': '', + }, + 'colors': [ + DEFAULT_COLORS[7], + DEFAULT_COLORS[0], + DEFAULT_COLORS[1], + ], + }, + 'gen_rad_session': { + 'type': 'stackedbar+lines', + 'calculate_total': True, + 'fill': 'none', + 'trace_type': user_signups_chart_traces, + 'trace_order': user_signups_chart_order, + 'title': _('Unique RADIUS Session Count'), + 'label': _('General RADIUS Session Count'), + 'description': _( + 'RADIUS Network traffic (total, download and upload).' + ), + 'summary_labels': user_signups_chart_summary_labels, + 'unit': '', + 'order': 243, + 'query': { + 'influxdb': ( + "SELECT COUNT(DISTINCT(username)) FROM {key} " + "WHERE time >= '{time}' {end_date} {organization_id} " + "{location_id} GROUP by time(1d), method" + ) + }, + 'query_default_param': { + 'organization_id': '', + 'location_id': '', + }, + 'colors': user_singups_chart_config['colors'], + }, + }, + }, +} diff --git a/openwisp_radius/integrations/monitoring/migrations/0001_initial.py b/openwisp_radius/integrations/monitoring/migrations/0001_initial.py new file mode 100644 index 00000000..f51954c0 --- /dev/null +++ b/openwisp_radius/integrations/monitoring/migrations/0001_initial.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.3 on 2023-08-09 13:56 + +import swapper +from django.db import migrations + +from . import create_general_metrics, delete_general_metrics + + +class Migration(migrations.Migration): + initial = True + dependencies = [ + swapper.dependency('monitoring', 'Metric'), + swapper.dependency('monitoring', 'Chart'), + ] + + operations = [ + migrations.RunPython( + create_general_metrics, reverse_code=delete_general_metrics + ) + ] diff --git a/openwisp_radius/integrations/monitoring/migrations/__init__.py b/openwisp_radius/integrations/monitoring/migrations/__init__.py new file mode 100644 index 00000000..179f4ff0 --- /dev/null +++ b/openwisp_radius/integrations/monitoring/migrations/__init__.py @@ -0,0 +1,42 @@ +import swapper + +from ..configuration import RADIUS_METRICS + + +def create_general_metrics(apps, schema_editor): + Chart = swapper.load_model('monitoring', 'Chart') + Metric = swapper.load_model('monitoring', 'Metric') + + metric, created = Metric._get_or_create( + configuration='user_signups', + name='User SignUps', + key='user_signups', + object_id=None, + content_type=None, + ) + if created: + for configuration in metric.config_dict['charts'].keys(): + chart = Chart(metric=metric, configuration=configuration) + chart.full_clean() + chart.save() + + metric, created = Metric._get_or_create( + configuration='gen_radius_acc', + name='RADIUS Accounting', + key='radius_acc', + object_id=None, + content_type=None, + ) + + if created: + for configuration in metric.config_dict['charts'].keys(): + chart = Chart(metric=metric, configuration=configuration) + chart.full_clean() + chart.save() + + +def delete_general_metrics(apps, schema_editor): + Metric = apps.get_model('monitoring', 'Metric') + Metric.objects.filter( + content_type__isnull=True, object_id__isnull=True, key__in=RADIUS_METRICS.keys() + ).delete() diff --git a/openwisp_radius/integrations/monitoring/receivers.py b/openwisp_radius/integrations/monitoring/receivers.py new file mode 100644 index 00000000..9548833d --- /dev/null +++ b/openwisp_radius/integrations/monitoring/receivers.py @@ -0,0 +1,38 @@ +from django.db import transaction + +from . import tasks + + +def post_save_registereduser(instance, created, *args, **kwargs): + if not created: + return + transaction.on_commit( + lambda: tasks.post_save_registereduser.delay( + user_id=str(instance.user_id), registration_method=instance.method + ) + ) + + +def post_save_organizationuser(instance, created, *args, **kwargs): + if not created: + return + transaction.on_commit( + lambda: tasks.post_save_organizationuser.delay( + user_id=str(instance.user_id), organization_id=str(instance.organization_id) + ) + ) + + +def post_save_radiusaccounting(instance, *args, **kwargs): + if instance.stop_time is None: + return + transaction.on_commit( + lambda: tasks.post_save_radiusaccounting.delay( + username=instance.username, + organization_id=str(instance.organization_id), + input_octets=instance.input_octets, + output_octets=instance.output_octets, + calling_station_id=instance.calling_station_id, + called_station_id=instance.called_station_id, + ) + ) diff --git a/openwisp_radius/integrations/monitoring/static/radius-monitoring/css/device-change.css b/openwisp_radius/integrations/monitoring/static/radius-monitoring/css/device-change.css new file mode 100644 index 00000000..61fac89d --- /dev/null +++ b/openwisp_radius/integrations/monitoring/static/radius-monitoring/css/device-change.css @@ -0,0 +1,14 @@ +#no-session-msg { + text-align: center; + margin-bottom: 20px; +} +#view-all-radius-session-wrapper { + text-align: center; + margin: 5px; +} +#radius-sessions th { + text-align: center; +} +#radius-session-tbody strong { + color: green; +} diff --git a/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js b/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js new file mode 100644 index 00000000..636385f9 --- /dev/null +++ b/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js @@ -0,0 +1,82 @@ +(function ($) { + 'use strict'; + const viewAllSessionMsg = gettext('View all RADIUS Sessions'); + const onlineMsg = gettext('online'); + const radiusSessionAdminPath = '/admin/openwisp_radius/radiusaccounting/' + $(document).ready(function () { + if (!$('#radius-sessions').length) { + return; + } + let deviceMac = encodeURIComponent($('#id_mac_address').val()), + apiEndpoint = `${radiusAccountingApiEndpoint}?called_station_id=${deviceMac}`; + + function getFormattedDateTimeString(dateTimeString) { + // Strip the timezone from the dateTimeString. + // THis is done to show the time in server's timezone + // because RadiusAccounting also shows the time in server's timezone. + let strippedDateTime = new Date(dateTimeString.substring(0, dateTimeString.lastIndexOf('-'))), + formattedDate = strippedDateTime.strftime('%d %b %Y, %I:%M %p'); + + return formattedDate.replace(/AM/g, 'a.m.').replace(/PM/g, 'p.m.'); + } + + function fetchRadiusSessions() { + if ($('#radius-session-tbody').children().length) { + return; + } + $.ajax({ + type: 'GET', + url: apiEndpoint, + xhrFields: { + withCredentials: true + }, + crossDomain: true, + beforeSend: function() { + $('#loading-overlay').show(); + }, + complete: function () { + $('#loading-overlay').fadeOut(250); + }, + success: function (response) { + if (response.length === 0) { + return; + } + $('#no-session-msg').hide() + $('#device-radius-sessions-table').show() + $('#view-all-radius-session-wrapper').show() + response.forEach(element => { + element.start_time = getFormattedDateTimeString(element.start_time) + if (!element.stop_time) { + element.stop_time = `${onlineMsg}`; + } else { + element.stop_time = getFormattedDateTimeString(element.stop_time); + } + $('#radius-session-tbody').append( + ` +

${element.session_id}

+

${element.username}

+

${element.input_octets}

+

${element.output_octets}

+

${element.called_station_id}

+

${element.start_time}

+

${element.stop_time}

+ ` + ); + }); + } + }) + } + $(document).on('tabshown', function (e) { + if (e.tabId === '#radius-sessions') { + fetchRadiusSessions() + } + }); + if (window.location.hash == '#radius-sessions') { + $.event.trigger({ + type: 'tabshown', + tabId: window.location.hash, + }); + } + $('#view-all-radius-session-wrapper a').attr('href', `${radiusAccountingAdminPath}?called_station_id=${deviceMac}`) + }); +}(django.jQuery)); diff --git a/openwisp_radius/integrations/monitoring/tasks.py b/openwisp_radius/integrations/monitoring/tasks.py new file mode 100644 index 00000000..34f54896 --- /dev/null +++ b/openwisp_radius/integrations/monitoring/tasks.py @@ -0,0 +1,148 @@ +import hashlib +import logging + +from celery import shared_task +from django.contrib.contenttypes.models import ContentType +from swapper import load_model + +Metric = load_model('monitoring', 'Metric') +Chart = load_model('monitoring', 'Chart') +RegisteredUser = load_model('openwisp_radius', 'RegisteredUser') +OrganizationUser = load_model('openwisp_users', 'OrganizationUser') +Device = load_model('config', 'Device') +DeviceLocation = load_model('geo', 'Location') + +logger = logging.getLogger(__name__) + + +def sha1_hash(input_string): + sha1 = hashlib.sha1() + sha1.update(input_string.encode('utf-8')) + return sha1.hexdigest() + + +def clean_registration_method(method): + if method == '': + method = 'unspecified' + return method + + +@shared_task +def post_save_registereduser(user_id, registration_method): + metric_data = [] + org_query = OrganizationUser.objects.filter(user_id=user_id).values_list( + 'organization_id', flat=True + ) + if not org_query: + logger.warning( + f'"{user_id}" is not a member of any organization.' + ' Skipping user_signup metric writing!' + ) + return + registration_method = clean_registration_method(registration_method) + for org_id in org_query: + metric, _ = Metric._get_or_create( + configuration='user_signups', + name='User SignUps', + key='user_signups', + object_id=None, + content_type=None, + extra_tags={ + 'organization_id': str(org_id), + 'method': registration_method, + }, + ) + metric_data.append((metric, {'value': sha1_hash(str(user_id))})) + Metric.batch_write(metric_data) + + +@shared_task +def post_save_organizationuser(user_id, organization_id): + try: + registration_method = ( + RegisteredUser.objects.only('method').get(user_id=user_id).method + ) + except RegisteredUser.DoesNotExist: + logger.warning( + f'RegisteredUser object not found for "{user_id}".' + ' Skipping user_signup metric writing!' + ) + return + registration_method = clean_registration_method(registration_method) + metric, _ = Metric._get_or_create( + configuration='user_signups', + name='User SignUps', + key='user_signups', + object_id=None, + content_type=None, + extra_tags={'organization_id': organization_id, 'method': registration_method}, + ) + metric.write(sha1_hash(str(user_id))) + + +@shared_task +def post_save_radiusaccounting( + username, + organization_id, + input_octets, + output_octets, + calling_station_id, + called_station_id, +): + try: + registration_method = ( + RegisteredUser.objects.only('method').get(user__username=username).method + ) + except RegisteredUser.DoesNotExist: + logger.warning( + f'RegisteredUser object not found for "{username}".' + ' Skipping radius_acc metric writing!' + ) + return + else: + registration_method = clean_registration_method(registration_method) + try: + device = ( + Device.objects.select_related('devicelocation') + .only('id', 'devicelocation__location_id') + .get(mac_address=called_station_id, organization_id=organization_id) + ) + except Device.DoesNotExist: + logger.warning( + f'Device object not found with MAC "{called_station_id}"' + f' and organization "{organization_id}".' + ' Skipping radius_acc metric writing!' + ) + return + device_id = str(device.id) + if hasattr(device, 'devicelocation'): + location_id = str(device.devicelocation.location_id) + else: + location_id = None + + metric, created = Metric._get_or_create( + configuration='radius_acc', + name='RADIUS Accounting', + key='radius_acc', + object_id=device_id, + content_type=ContentType.objects.get_for_model(Device), + extra_tags={ + 'organization_id': organization_id, + 'method': registration_method, + 'calling_station_id': calling_station_id, + 'called_station_id': called_station_id, + 'location_id': location_id, + }, + ) + metric.write( + input_octets, + extra_values={ + 'output_octets': output_octets, + 'username': sha1_hash(username), + }, + ) + if created: + for configuration in metric.config_dict['charts'].keys(): + chart = Chart(metric=metric, configuration=configuration) + chart.full_clean() + chart.save() diff --git a/openwisp_radius/integrations/monitoring/templates/admin/config/radius-monitoring/device/change_form.html b/openwisp_radius/integrations/monitoring/templates/admin/config/radius-monitoring/device/change_form.html new file mode 100644 index 00000000..f8e646a3 --- /dev/null +++ b/openwisp_radius/integrations/monitoring/templates/admin/config/radius-monitoring/device/change_form.html @@ -0,0 +1,38 @@ +{% extends "admin/config/device/change_form.html" %} +{% load i18n %} + +{% block inline_field_sets %} +{{ block.super }} +{% comment %} +RadiusAccounting model does not have a ForeignKey to Device object. +Therefore, RadiusAccounting inline is added using HTML and JS. +{% endcomment %} +{% if not add %} +
+

{% trans "There are no RADIUS sessions for this device." %}

+ +
+ +{% endif %} +{% endblock %} + diff --git a/openwisp_radius/registration.py b/openwisp_radius/registration.py index e797e4fe..66d26921 100644 --- a/openwisp_radius/registration.py +++ b/openwisp_radius/registration.py @@ -8,8 +8,8 @@ REGISTRATION_METHOD_CHOICES = [ ('', 'Unspecified'), ('manual', _('Manually created')), - ('email', _('Email (No Identity Verification)')), - ('mobile_phone', _('Mobile phone verification')), + ('email', _('Email')), + ('mobile_phone', _('Mobile phone')), ] AUTHORIZE_UNVERIFIED = [] diff --git a/openwisp_radius/tests/test_api/test_api.py b/openwisp_radius/tests/test_api/test_api.py index 1e3bc997..6224fa30 100644 --- a/openwisp_radius/tests/test_api/test_api.py +++ b/openwisp_radius/tests/test_api/test_api.py @@ -39,6 +39,7 @@ OrganizationRadiusSettings = load_model('OrganizationRadiusSettings') Organization = swapper.load_model('openwisp_users', 'Organization') OrganizationUser = swapper.load_model('openwisp_users', 'OrganizationUser') +Group = swapper.load_model('openwisp_users', 'Group') START_DATE = '2019-04-20T22:14:09+01:00' @@ -1173,6 +1174,98 @@ def test_user_radius_usage_view(self): response = self.client.get(usage_url, HTTP_AUTHORIZATION=authorization) self.assertEqual(response.status_code, 404) + def test_user_group_check_serializer_counter_does_not_exist(self): + group = self._create_radius_group(name='group name') + group_check = self._create_radius_groupcheck( + attribute='ChilliSpot-Max-Input-Octets', + op=':=', + value='2000000000', + group=group, + groupname=group.name, + ) + serializer = UserGroupCheckSerializer(group_check) + self.assertDictEqual( + serializer.data, + { + 'attribute': 'ChilliSpot-Max-Input-Octets', + 'op': ':=', + 'result': None, + 'type': None, + 'value': '2000000000', + }, + ) + + def test_radius_accounting(self): + path = reverse('radius:radius_accounting_list') + org1 = self.default_org + org2 = self._create_org(name='org2', slug='org2') + data1 = self.acct_post_data + data1.update( + dict( + session_id='35000006', + unique_id='75058e50', + input_octets=9900909, + output_octets=1513075509, + username='tester', + organization=org1, + ) + ) + self._create_radius_accounting(**data1) + data2 = self.acct_post_data + data2.update( + dict( + session_id='40111116', + unique_id='12234f69', + input_octets=3000909, + output_octets=1613176609, + username='tester', + organization=org1, + ) + ) + self._create_radius_accounting(**data2) + data3 = self.acct_post_data + data3.update( + dict( + session_id='89897654', + unique_id='99144d60', + input_octets=4440909, + output_octets=1119074409, + username='admin', + organization=org2, + ) + ) + self._create_radius_accounting(**data3) + + with self.subTest('Test unauthenicated user'): + response = self.client.get(path) + self.assertEqual(response.status_code, 401) + + org1_user = self._create_org_user(organization=org1, is_admin=False) + administrator = Group.objects.get(name='Administrator') + org1_user.user.groups.add(administrator) + self.client.force_login(org1_user.user) + with self.subTest('Test organization user (not organization manager)'): + response = self.client.get(path) + self.assertEqual(response.status_code, 403) + + org1_user.is_admin = True + org1_user.save() + with self.subTest('Test organization user (organization manager)'): + response = self.client.get(path) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 2) + self.assertEqual(response.data[0]['unique_id'], data2['unique_id']) + self.assertEqual(response.data[1]['unique_id'], data1['unique_id']) + + with self.subTest('Test superuser can view all sessions'): + admin = self._create_admin() + self.client.force_login(admin) + response = self.client.get(path) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 3) + self.assertEqual(response.data[0]['unique_id'], data3['unique_id']) + self.assertEqual(response.data[1]['unique_id'], data2['unique_id']) + self.assertEqual(response.data[2]['unique_id'], data1['unique_id']) del BaseTestCase del BaseTransactionTestCase diff --git a/setup.cfg b/setup.cfg index 17920818..d04257c7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,5 +11,5 @@ exclude = *.egg-info, .git, ./tests/*settings*.py, docs/* - ./tests/openwisp2/saml/* + ./tests/openwisp2/saml/* max-line-length = 88 diff --git a/tests/openwisp2/routing.py b/tests/openwisp2/routing.py new file mode 100644 index 00000000..8f1973a4 --- /dev/null +++ b/tests/openwisp2/routing.py @@ -0,0 +1,12 @@ +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator +from openwisp_controller.routing import get_routes + +application = ProtocolTypeRouter( + { + 'websocket': AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(get_routes())) + ) + } +) diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py index 1644021c..c4b653f0 100644 --- a/tests/openwisp2/settings.py +++ b/tests/openwisp2/settings.py @@ -9,7 +9,7 @@ # Set DEBUG to False in production DEBUG = True - +INTERNAL_IPS = ['127.0.0.1'] SECRET_KEY = '&a@f(0@lrl%606smticbu20=pvribdvubk5=gjti8&n1y%bi&4' ALLOWED_HOSTS = [] @@ -24,35 +24,53 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', - # openwisp admin theme - 'openwisp_utils.admin_theme', - 'openwisp_users.accounts', + 'django.contrib.gis', # all-auth 'django.contrib.sites', + # overrides allauth templates + # must precede allauth + 'openwisp_users.accounts', 'allauth', 'allauth.account', 'allauth.socialaccount', - # rest framework - 'rest_framework', - 'django_filters', - # registration - 'rest_framework.authtoken', + 'django_extensions', + # openwisp2 modules + 'openwisp_users', + 'openwisp_controller.pki', + 'openwisp_controller.config', + 'openwisp_controller.geo', + 'openwisp_controller.connection', + 'openwisp_ipam', + 'openwisp_monitoring.monitoring', + 'openwisp_monitoring.device', + 'openwisp_monitoring.check', + 'nested_admin', + 'openwisp_notifications', + 'flat_json_widget', 'dj_rest_auth', 'dj_rest_auth.registration', - # social login - 'allauth.socialaccount.providers.facebook', - 'allauth.socialaccount.providers.google', - # openwisp radius 'openwisp_radius', - 'openwisp_users', - # admin + 'openwisp_radius.integrations.monitoring', + # openwisp2 admin theme + # (must be loaded here) + 'openwisp_utils.admin_theme', 'admin_auto_filters', + # admin 'django.contrib.admin', + 'django.forms', + # other dependencies + 'sortedm2m', + 'reversion', + 'leaflet', + 'rest_framework', + 'rest_framework_gis', + 'rest_framework.authtoken', + 'django_filters', 'private_storage', 'drf_yasg', - 'django_extensions', - 'openwisp2.integrations', - 'djangosaml2', + 'import_export', + 'channels', + # 'debug_toolbar', ] LOGIN_REDIRECT_URL = 'admin:index' @@ -83,6 +101,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'djangosaml2.middleware.SamlSessionMiddleware', + # 'debug_toolbar.middleware.DebugToolbarMiddleware', ] SESSION_COOKIE_SECURE = True @@ -101,8 +120,8 @@ 'OPTIONS': { 'loaders': [ 'django.template.loaders.filesystem.Loader', - 'openwisp_utils.loaders.DependencyLoader', 'django.template.loaders.app_directories.Loader', + 'openwisp_utils.loaders.DependencyLoader', ], 'context_processors': [ 'django.template.context_processors.debug', @@ -110,6 +129,7 @@ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'openwisp_utils.admin_theme.context_processor.menu_groups', + 'openwisp_notifications.context_processors.notification_api_settings', ], }, } @@ -220,7 +240,7 @@ ), } -if not TESTING: +if TESTING: CELERY_BROKER_URL = os.getenv('REDIS_URL', f'redis://{redis_host}/1') else: OPENWISP_RADIUS_GROUPCHECK_ADMIN = True @@ -305,12 +325,36 @@ OPENWISP_USERS_AUTH_API = True +TIMESERIES_DATABASE = { + 'BACKEND': 'openwisp_monitoring.db.backends.influxdb', + 'USER': 'openwisp', + 'PASSWORD': 'openwisp', + 'NAME': 'openwisp2', + 'HOST': os.getenv('INFLUXDB_HOST', 'localhost'), + 'PORT': '8086', + # UDP writes are disabled by default + 'OPTIONS': {'udp_writes': False, 'udp_port': 8089}, +} +EXTENDED_APPS = ['django_x509', 'django_loci'] + +ASGI_APPLICATION = 'openwisp2.routing.application' +if TESTING: + CHANNEL_LAYERS = {'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'}} +else: + CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': {'hosts': [f'redis://{redis_host}/7']}, + } + } + + if os.environ.get('SAMPLE_APP', False): INSTALLED_APPS.remove('openwisp_radius') INSTALLED_APPS.remove('openwisp_users') INSTALLED_APPS.append('openwisp2.sample_radius') INSTALLED_APPS.append('openwisp2.sample_users') - EXTENDED_APPS = ('openwisp_radius', 'openwisp_users') + # EXTENDED_APPS = ('openwisp_radius', 'openwisp_users') AUTH_USER_MODEL = 'sample_users.User' OPENWISP_USERS_GROUP_MODEL = 'sample_users.Group' OPENWISP_USERS_ORGANIZATION_MODEL = 'sample_users.Organization' @@ -363,3 +407,16 @@ from .local_settings import * except ImportError: pass + +FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' + +if not TESTING: + CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://127.0.0.1:6379/6', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + }, + } + } diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index a9465644..d42d307e 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -29,6 +29,8 @@ urlpatterns = [ path('admin/', admin.site.urls), + path('', include('openwisp_controller.urls')), + path('', include('openwisp_monitoring.urls')), path('api/v1/', include('openwisp_utils.api.urls')), path('api/v1/', include('openwisp_users.api.urls')), path('accounts/', include('openwisp_users.accounts.urls')), @@ -46,3 +48,8 @@ ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += staticfiles_urlpatterns() + +if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS: + import debug_toolbar + + urlpatterns.append(path('__debug__/', include(debug_toolbar.urls))) diff --git a/tests/openwisp2/views.py b/tests/openwisp2/views.py index 8071cd7f..d916ebb6 100644 --- a/tests/openwisp2/views.py +++ b/tests/openwisp2/views.py @@ -1,4 +1,5 @@ import logging +import random import uuid from urllib.parse import urlparse @@ -53,7 +54,8 @@ def captive_portal_login(request): session_id=id_, 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', + # called_station_id='E8:48:B8:80:24:BC', + called_station_id='11:22:33:44:55:66', session_time=0, input_octets=0, output_octets=0, @@ -81,6 +83,8 @@ def captive_portal_logout(request): terminate_cause='User-Request', nas_ip_address='127.0.0.1', unique_id=session_id, + input_octets=random.randint(20, 50) * 100000, + output_octets=random.randint(20, 50) * 100000, ) post_accounting_data(request, data) logger.info( From a6c90c201a8f4632e27dec00b14702d1f621eca0 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 31 Aug 2023 10:12:57 +0530 Subject: [PATCH 02/51] [req-changes] Fixed colours for RADIUS charts --- openwisp_radius/integrations/monitoring/configuration.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openwisp_radius/integrations/monitoring/configuration.py b/openwisp_radius/integrations/monitoring/configuration.py index 46da43f7..92acad91 100644 --- a/openwisp_radius/integrations/monitoring/configuration.py +++ b/openwisp_radius/integrations/monitoring/configuration.py @@ -42,12 +42,13 @@ }, 'colors': [ DEFAULT_COLORS[7], - DEFAULT_COLORS[0], - DEFAULT_COLORS[1], - DEFAULT_COLORS[2], - DEFAULT_COLORS[3], DEFAULT_COLORS[4], + DEFAULT_COLORS[3], DEFAULT_COLORS[5], + DEFAULT_COLORS[0], + DEFAULT_COLORS[2], + DEFAULT_COLORS[1], + DEFAULT_COLORS[6], ], } From 7afde86acbcd493be4e557981100f7f5c0f0b048 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 31 Aug 2023 11:01:39 +0530 Subject: [PATCH 03/51] [chores] Refactored code --- .jshintrc | 7 +++++ .../radius-monitoring/css/device-change.css | 3 -- .../radius-monitoring/js/device-change.js | 28 ++++++++++--------- .../radius-monitoring/device/change_form.html | 3 +- .../openwisp-radius/js/mode-switcher.js | 2 +- .../openwisp-radius/js/strategy-switcher.js | 2 +- run-qa-checks | 2 ++ setup.cfg | 1 - tests/openwisp2/views.py | 3 +- 9 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 .jshintrc diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..99b4b79c --- /dev/null +++ b/.jshintrc @@ -0,0 +1,7 @@ +{ + "unused": true, + "esversion": 6, + "curly": true, + "strict": "global", + "browser": true +} diff --git a/openwisp_radius/integrations/monitoring/static/radius-monitoring/css/device-change.css b/openwisp_radius/integrations/monitoring/static/radius-monitoring/css/device-change.css index 61fac89d..36cad3fa 100644 --- a/openwisp_radius/integrations/monitoring/static/radius-monitoring/css/device-change.css +++ b/openwisp_radius/integrations/monitoring/static/radius-monitoring/css/device-change.css @@ -6,9 +6,6 @@ text-align: center; margin: 5px; } -#radius-sessions th { - text-align: center; -} #radius-session-tbody strong { color: green; } diff --git a/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js b/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js index 636385f9..3c87d094 100644 --- a/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js +++ b/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js @@ -1,27 +1,29 @@ (function ($) { 'use strict'; - const viewAllSessionMsg = gettext('View all RADIUS Sessions'); + const onlineMsg = gettext('online'); - const radiusSessionAdminPath = '/admin/openwisp_radius/radiusaccounting/' + $(document).ready(function () { if (!$('#radius-sessions').length) { + // RADIUS sessions tab should not appear on Device add page. return; } - let deviceMac = encodeURIComponent($('#id_mac_address').val()), + const deviceMac = encodeURIComponent($('#id_mac_address').val()), apiEndpoint = `${radiusAccountingApiEndpoint}?called_station_id=${deviceMac}`; function getFormattedDateTimeString(dateTimeString) { // Strip the timezone from the dateTimeString. - // THis is done to show the time in server's timezone - // because RadiusAccounting also shows the time in server's timezone. + // This is done to show the time in server's timezone + // because RadiusAccounting admin also shows the time in server's timezone. let strippedDateTime = new Date(dateTimeString.substring(0, dateTimeString.lastIndexOf('-'))), formattedDate = strippedDateTime.strftime('%d %b %Y, %I:%M %p'); - return formattedDate.replace(/AM/g, 'a.m.').replace(/PM/g, 'p.m.'); } function fetchRadiusSessions() { if ($('#radius-session-tbody').children().length) { + // Don't fetch if RADIUS sessions are already present + // in the table return; } $.ajax({ @@ -41,11 +43,11 @@ if (response.length === 0) { return; } - $('#no-session-msg').hide() - $('#device-radius-sessions-table').show() - $('#view-all-radius-session-wrapper').show() + $('#no-session-msg').hide(); + $('#device-radius-sessions-table').show(); + $('#view-all-radius-session-wrapper').show(); response.forEach(element => { - element.start_time = getFormattedDateTimeString(element.start_time) + element.start_time = getFormattedDateTimeString(element.start_time); if (!element.stop_time) { element.stop_time = `${onlineMsg}`; } else { @@ -64,11 +66,11 @@ ); }); } - }) + }); } $(document).on('tabshown', function (e) { if (e.tabId === '#radius-sessions') { - fetchRadiusSessions() + fetchRadiusSessions(); } }); if (window.location.hash == '#radius-sessions') { @@ -77,6 +79,6 @@ tabId: window.location.hash, }); } - $('#view-all-radius-session-wrapper a').attr('href', `${radiusAccountingAdminPath}?called_station_id=${deviceMac}`) + $('#view-all-radius-session-wrapper a').attr('href', `${radiusAccountingAdminPath}?called_station_id=${deviceMac}`); }); }(django.jQuery)); diff --git a/openwisp_radius/integrations/monitoring/templates/admin/config/radius-monitoring/device/change_form.html b/openwisp_radius/integrations/monitoring/templates/admin/config/radius-monitoring/device/change_form.html index f8e646a3..02c288ed 100644 --- a/openwisp_radius/integrations/monitoring/templates/admin/config/radius-monitoring/device/change_form.html +++ b/openwisp_radius/integrations/monitoring/templates/admin/config/radius-monitoring/device/change_form.html @@ -12,7 +12,7 @@

{% trans "There are no RADIUS sessions for this device." %}

{% endif %} {% endblock %} diff --git a/openwisp_radius/integrations/monitoring/tests/test_admin.py b/openwisp_radius/integrations/monitoring/tests/test_admin.py index 7a87c429..1e41dc6a 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_admin.py +++ b/openwisp_radius/integrations/monitoring/tests/test_admin.py @@ -1,13 +1,15 @@ from django.test import TestCase, tag from django.urls import reverse +from django.utils import timezone -from openwisp_users.tests.utils import TestOrganizationMixin +from openwisp_radius.tests import _RADACCT, CreateRadiusObjectsMixin +from ..utils import get_datetime_filter_start_date, get_datetime_filter_stop_date from .mixins import CreateDeviceMonitoringMixin @tag('radius_monitoring') -class TestDeviceAdmin(TestOrganizationMixin, CreateDeviceMonitoringMixin, TestCase): +class TestDeviceAdmin(CreateRadiusObjectsMixin, CreateDeviceMonitoringMixin, TestCase): app_label = 'config' def setUp(self): @@ -28,3 +30,33 @@ def test_radius_session_tab(self): response, '', ) + + def test_radius_dashboard_chart_data(self): + options = _RADACCT.copy() + options['unique_id'] = '117' + options['start_time'] = timezone.now().strftime('%Y-%m-%d 00:00:00') + options['input_octets'] = '1234567890' + self._create_radius_accounting(**options) + response = self.client.get(reverse('admin:index')) + self.assertEqual(response.status_code, 200) + start_time = get_datetime_filter_start_date() + end_time = get_datetime_filter_stop_date() + self.assertContains( + response, + ( + '{\'name\': "Today\'s RADIUS sessions", \'query_params\': ' + '{\'values\': [1], \'labels\': [\'Open\']}, \'colors\': ' + '[\'#267126\'], \'filters\': [\'True\'], \'labels\': ' + '{\'open\': \'Open\', \'closed\': \'Closed\'}, \'target_link\': ' + '\'/admin/openwisp_radius/radiusaccounting/?start_time__gte=' + f'{start_time}&start_time__lt={end_time}' + '&stop_time__isnull=\'}, 31: {\'name\': "Today\'s RADIUS traffic (GB)",' + ' \'query_params\': {\'values\': [1.0], \'labels\': ' + '[\'Download traffic (GB)\']}, \'colors\': [\'#1f77b4\'], \'labels\': ' + '{\'download_traffic\': \'Download traffic (GB)\', \'upload_traffic\': ' + '\'Upload traffic (GB)\'}, \'filtering\': \'False\', \'target_link\': ' + '\'/admin/openwisp_radius/radiusaccounting/?start_time__gte=' + f'{start_time}&start_time__lt={end_time}\'' + '}' + ), + ) diff --git a/openwisp_radius/integrations/monitoring/tests/test_metrics.py b/openwisp_radius/integrations/monitoring/tests/test_metrics.py index 6f95f3df..2172581c 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_metrics.py +++ b/openwisp_radius/integrations/monitoring/tests/test_metrics.py @@ -85,6 +85,14 @@ def test_post_save_radiusaccounting(self, *args): self.assertEqual(points['traces'][0][1][-1], 1) self.assertEqual(points['summary'], {'mobile_phone': 1}) + @patch('openwisp_radius.integrations.monitoring.tasks.post_save_radiusaccounting') + def test_post_save_radiusaccouting_open_session(self, mocked_task): + radius_options = _RADACCT.copy() + radius_options['unique_id'] = '117' + session = self._create_radius_accounting(**radius_options) + self.assertEqual(session.stop_time, None) + mocked_task.assert_not_called() + @patch('logging.Logger.warning') def test_post_save_radius_accounting_device_not_found(self, mocked_logger): """ From 5c06646caef2d4a8663e9ac3b89b3f18619849eb Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 1 Apr 2024 20:43:24 +0530 Subject: [PATCH 34/51] [req-changes] Updated order of RADIUS charts in device page --- openwisp_radius/integrations/monitoring/configuration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openwisp_radius/integrations/monitoring/configuration.py b/openwisp_radius/integrations/monitoring/configuration.py index b69293f0..d1679b07 100644 --- a/openwisp_radius/integrations/monitoring/configuration.py +++ b/openwisp_radius/integrations/monitoring/configuration.py @@ -112,7 +112,7 @@ _('Total upload traffic'), ], 'unit': 'adaptive_prefix+B', - 'order': 241, + 'order': 221, 'query': { 'influxdb': ( "SELECT SUM(output_octets) / 1000000000 AS upload, " @@ -142,7 +142,7 @@ ), 'summary_labels': user_signups_chart_summary_labels, 'unit': '', - 'order': 242, + 'order': 222, 'query': { 'influxdb': ( "SELECT COUNT(DISTINCT(username)) FROM {key} " From fdfb63fa0d82b74a04e431b5cc37534355d1e2b9 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 4 Apr 2024 23:12:47 +0530 Subject: [PATCH 35/51] [req-change] Use verbose label for SAML in radius monitoring charts --- openwisp_radius/integrations/monitoring/configuration.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openwisp_radius/integrations/monitoring/configuration.py b/openwisp_radius/integrations/monitoring/configuration.py index d1679b07..978814dd 100644 --- a/openwisp_radius/integrations/monitoring/configuration.py +++ b/openwisp_radius/integrations/monitoring/configuration.py @@ -4,9 +4,14 @@ from openwisp_monitoring.monitoring.configuration import DEFAULT_COLORS from openwisp_radius.registration import REGISTRATION_METHOD_CHOICES +from openwisp_radius.settings import SAML_REGISTRATION_METHOD_LABEL user_signups_chart_traces = {'total': 'lines'} user_signups_chart_order = ['total'] +user_signups_chart_trace_labels = { + 'total': _('Total'), + 'saml': SAML_REGISTRATION_METHOD_LABEL, +} user_signups_chart_summary_labels = [_('Total new users')] for method, label in REGISTRATION_METHOD_CHOICES: @@ -27,6 +32,7 @@ 'label': _('User Registration'), 'description': _('Daily user registration grouped by registration method'), 'summary_labels': user_signups_chart_summary_labels, + 'trace_labels': user_signups_chart_trace_labels, 'order': 240, '__all__': True, 'unit': '', @@ -141,6 +147,7 @@ 'RADIUS Network traffic (total, download and upload).' ), 'summary_labels': user_signups_chart_summary_labels, + 'trace_labels': user_signups_chart_trace_labels, 'unit': '', 'order': 222, 'query': { @@ -219,6 +226,7 @@ 'RADIUS Network traffic (total, download and upload).' ), 'summary_labels': user_signups_chart_summary_labels, + 'trace_labels': user_signups_chart_trace_labels, 'unit': '', 'order': 243, 'query': { From 876000343f5d3aaac2db9b7b0e1903360885dbbc Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 4 Apr 2024 23:45:02 +0530 Subject: [PATCH 36/51] [req-change] Use verbose name for all registration methods --- openwisp_radius/integrations/monitoring/configuration.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openwisp_radius/integrations/monitoring/configuration.py b/openwisp_radius/integrations/monitoring/configuration.py index 978814dd..530909ca 100644 --- a/openwisp_radius/integrations/monitoring/configuration.py +++ b/openwisp_radius/integrations/monitoring/configuration.py @@ -4,13 +4,11 @@ from openwisp_monitoring.monitoring.configuration import DEFAULT_COLORS from openwisp_radius.registration import REGISTRATION_METHOD_CHOICES -from openwisp_radius.settings import SAML_REGISTRATION_METHOD_LABEL user_signups_chart_traces = {'total': 'lines'} user_signups_chart_order = ['total'] user_signups_chart_trace_labels = { 'total': _('Total'), - 'saml': SAML_REGISTRATION_METHOD_LABEL, } user_signups_chart_summary_labels = [_('Total new users')] @@ -21,6 +19,7 @@ user_signups_chart_summary_labels.append( _('New %(label)s users' % {"label": label}) ) + user_signups_chart_trace_labels[method] = label user_signups_chart_order.append(method) From 7cea5730399c1de167165cb41253084ece262858 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 17 Apr 2024 08:32:37 +0700 Subject: [PATCH 37/51] [fix] Fixed date formatting for RADIUS sessions on device page --- .../monitoring/static/radius-monitoring/js/device-change.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js b/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js index 78adab56..47aac605 100644 --- a/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js +++ b/openwisp_radius/integrations/monitoring/static/radius-monitoring/js/device-change.js @@ -18,7 +18,7 @@ // Strip the timezone from the dateTimeString. // This is done to show the time in server's timezone // because RadiusAccounting admin also shows the time in server's timezone. - let strippedDateTime = new Date(dateTimeString.substring(0, dateTimeString.lastIndexOf('-'))); + let strippedDateTime = new Date(dateTimeString.replace(/[-+]\d{2}:\d{2}$/, '')); return strippedDateTime.toLocaleString(); } From db093e46af42b54f7a829a94446747862adeec80 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 17 Apr 2024 23:18:27 +0700 Subject: [PATCH 38/51] [fix] Fixed metric not writing when RegisteredUser does not exist --- .../integrations/monitoring/tasks.py | 4 +- .../monitoring/tests/test_metrics.py | 74 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/openwisp_radius/integrations/monitoring/tasks.py b/openwisp_radius/integrations/monitoring/tasks.py index 4d9a47e4..09bb1ad6 100644 --- a/openwisp_radius/integrations/monitoring/tasks.py +++ b/openwisp_radius/integrations/monitoring/tasks.py @@ -198,9 +198,9 @@ def post_save_radiusaccounting( except RegisteredUser.DoesNotExist: logger.warning( f'RegisteredUser object not found for "{username}".' - ' Skipping radius_acc metric writing!' + ' The metric will be written with "unspecified" registration method!' ) - return + registration_method = 'unspecified' else: registration_method = clean_registration_method(registration_method) diff --git a/openwisp_radius/integrations/monitoring/tests/test_metrics.py b/openwisp_radius/integrations/monitoring/tests/test_metrics.py index 2172581c..30c9210f 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_metrics.py +++ b/openwisp_radius/integrations/monitoring/tests/test_metrics.py @@ -169,6 +169,80 @@ def test_post_save_radius_accounting_device_not_found(self, mocked_logger): ' The metric will be written without a related object!' ) + @patch('logging.Logger.warning') + def test_post_save_radius_accounting_registereduser_not_found(self, mocked_logger): + """ + This test checks that radius accounting metric is created + even if the RegisteredUser object could not be found for the user. + This scenario can happen on an installations which do not require + users to signup to access the internet/ + """ + user = self._create_user() + device = self._create_device() + options = _RADACCT.copy() + options.update( + { + 'unique_id': '117', + 'username': user.username, + 'called_station_id': device.mac_address.replace('-', ':').upper(), + 'calling_station_id': '00:00:00:00:00:00', + 'input_octets': '8000000000', + 'output_octets': '9000000000', + } + ) + options['stop_time'] = options['start_time'] + # Remove calls for user registration from mocked logger + mocked_logger.reset_mock() + + self._create_radius_accounting(**options) + self.assertEqual( + self.metric_model.objects.filter( + configuration='radius_acc', + name='RADIUS Accounting', + key='radius_acc', + object_id=str(device.id), + content_type=ContentType.objects.get_for_model(self.device_model), + extra_tags={ + 'called_station_id': device.mac_address, + 'calling_station_id': '00:00:00:00:00:00', + 'location_id': None, + 'method': 'unspecified', + 'organization_id': str(self.default_org.id), + }, + ).count(), + 1, + ) + # The TransactionTestCase truncates all the data after each test. + # The general metrics and charts which are created by migrations + # get deleted after each test. Therefore, we create them again here. + create_general_metrics(None, None) + metric = self.metric_model.objects.filter(configuration='radius_acc').first() + # A dedicated chart for this metric was not created since the + # related device was not identified by the called_station_id. + # The data however can be retrieved from the general charts. + self.assertEqual(metric.chart_set.count(), 2) + general_traffic_chart = self.chart_model.objects.get( + configuration='gen_rad_traffic' + ) + points = general_traffic_chart.read() + self.assertEqual(points['traces'][0][0], 'download') + self.assertEqual(points['traces'][0][1][-1], 8) + self.assertEqual(points['traces'][1][0], 'upload') + self.assertEqual(points['traces'][1][1][-1], 9) + self.assertEqual(points['summary'], {'upload': 9, 'download': 8}) + + general_session_chart = self.chart_model.objects.get( + configuration='gen_rad_session' + ) + points = general_session_chart.read() + self.assertEqual(points['traces'][0][0], 'unspecified') + self.assertEqual(points['traces'][0][1][-1], 1) + self.assertEqual(points['summary'], {'unspecified': 1}) + mocked_logger.assert_called_once_with( + f'RegisteredUser object not found for "{user.username}".' + ' The metric will be written with "unspecified" registration method!' + ) + def test_write_user_registration_metrics(self): from ..tasks import write_user_registration_metrics From fe973a5c9fe8879a95f09e37eb2e66c6af98f7f3 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 18 Apr 2024 00:10:58 +0700 Subject: [PATCH 39/51] [fix] Fixed test --- .../integrations/monitoring/tasks.py | 2 +- .../monitoring/tests/test_metrics.py | 30 +++++++------------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/openwisp_radius/integrations/monitoring/tasks.py b/openwisp_radius/integrations/monitoring/tasks.py index 09bb1ad6..21936c1d 100644 --- a/openwisp_radius/integrations/monitoring/tasks.py +++ b/openwisp_radius/integrations/monitoring/tasks.py @@ -196,7 +196,7 @@ def post_save_radiusaccounting( RegisteredUser.objects.only('method').get(user__username=username).method ) except RegisteredUser.DoesNotExist: - logger.warning( + logger.info( f'RegisteredUser object not found for "{username}".' ' The metric will be written with "unspecified" registration method!' ) diff --git a/openwisp_radius/integrations/monitoring/tests/test_metrics.py b/openwisp_radius/integrations/monitoring/tests/test_metrics.py index 30c9210f..65c2022e 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_metrics.py +++ b/openwisp_radius/integrations/monitoring/tests/test_metrics.py @@ -169,7 +169,7 @@ def test_post_save_radius_accounting_device_not_found(self, mocked_logger): ' The metric will be written without a related object!' ) - @patch('logging.Logger.warning') + @patch('logging.Logger.info') def test_post_save_radius_accounting_registereduser_not_found(self, mocked_logger): """ This test checks that radius accounting metric is created @@ -179,6 +179,10 @@ def test_post_save_radius_accounting_registereduser_not_found(self, mocked_logge """ user = self._create_user() device = self._create_device() + device_loc = self._create_device_location( + content_object=device, + location=self._create_location(organization=device.organization), + ) options = _RADACCT.copy() options.update( { @@ -191,8 +195,6 @@ def test_post_save_radius_accounting_registereduser_not_found(self, mocked_logge } ) options['stop_time'] = options['start_time'] - # Remove calls for user registration from mocked logger - mocked_logger.reset_mock() self._create_radius_accounting(**options) self.assertEqual( @@ -205,36 +207,24 @@ def test_post_save_radius_accounting_registereduser_not_found(self, mocked_logge extra_tags={ 'called_station_id': device.mac_address, 'calling_station_id': '00:00:00:00:00:00', - 'location_id': None, + 'location_id': str(device_loc.location.id), 'method': 'unspecified', 'organization_id': str(self.default_org.id), }, ).count(), 1, ) - # The TransactionTestCase truncates all the data after each test. - # The general metrics and charts which are created by migrations - # get deleted after each test. Therefore, we create them again here. - create_general_metrics(None, None) metric = self.metric_model.objects.filter(configuration='radius_acc').first() - # A dedicated chart for this metric was not created since the - # related device was not identified by the called_station_id. - # The data however can be retrieved from the general charts. - self.assertEqual(metric.chart_set.count(), 2) - general_traffic_chart = self.chart_model.objects.get( - configuration='gen_rad_traffic' - ) - points = general_traffic_chart.read() + traffic_chart = metric.chart_set.get(configuration='radius_traffic') + points = traffic_chart.read() self.assertEqual(points['traces'][0][0], 'download') self.assertEqual(points['traces'][0][1][-1], 8) self.assertEqual(points['traces'][1][0], 'upload') self.assertEqual(points['traces'][1][1][-1], 9) self.assertEqual(points['summary'], {'upload': 9, 'download': 8}) - general_session_chart = self.chart_model.objects.get( - configuration='gen_rad_session' - ) - points = general_session_chart.read() + session_chart = metric.chart_set.get(configuration='rad_session') + points = session_chart.read() self.assertEqual(points['traces'][0][0], 'unspecified') self.assertEqual(points['traces'][0][1][-1], 1) self.assertEqual(points['summary'], {'unspecified': 1}) From f9e705425a7715f2c3df08c53ac1f3a353cbd0b8 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 18 Apr 2024 18:54:49 +0700 Subject: [PATCH 40/51] [req-changes] Removed fill from User Registration form --- openwisp_radius/integrations/monitoring/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openwisp_radius/integrations/monitoring/configuration.py b/openwisp_radius/integrations/monitoring/configuration.py index 530909ca..b47d3bef 100644 --- a/openwisp_radius/integrations/monitoring/configuration.py +++ b/openwisp_radius/integrations/monitoring/configuration.py @@ -40,7 +40,7 @@ 'influxdb': ( "SELECT SUM(count) FROM " " {key} WHERE time >= '{time}' {end_date} {organization_id}" - " GROUP BY time(1d), method FILL(linear)" + " GROUP BY time(1d), method" ) }, 'query_default_param': { From e19b7264552e220adfcb49ad34b6f0561d325d66 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 18 Apr 2024 19:03:12 +0700 Subject: [PATCH 41/51] [req-changes] Renamed labels of RADIUS monitoring charts - Rename "User Registration" to "User Registrations" - Rename "Unique RADIUS Session Count" to "Unique RADIUS Sessions" - Rename "Total RADIUS Sessions Traffic" to "Traffic of RADIUS Sessions" --- .../integrations/monitoring/configuration.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openwisp_radius/integrations/monitoring/configuration.py b/openwisp_radius/integrations/monitoring/configuration.py index b47d3bef..35c1df6c 100644 --- a/openwisp_radius/integrations/monitoring/configuration.py +++ b/openwisp_radius/integrations/monitoring/configuration.py @@ -27,9 +27,9 @@ 'type': 'stackedbar+lines', 'trace_type': user_signups_chart_traces, 'trace_order': user_signups_chart_order, - 'title': _('User Registration'), - 'label': _('User Registration'), - 'description': _('Daily user registration grouped by registration method'), + 'title': _('User Registrations'), + 'label': _('User Registrations'), + 'description': _('Daily user registrations grouped by registration method'), 'summary_labels': user_signups_chart_summary_labels, 'trace_labels': user_signups_chart_trace_labels, 'order': 240, @@ -73,8 +73,8 @@ RADIUS_METRICS = { 'user_signups': { - 'label': _('User Registration'), - 'name': 'User Registration', + 'label': _('User Registrations'), + 'name': 'User Registrations', 'key': 'user_signups', 'field_name': 'count', 'charts': { @@ -82,8 +82,8 @@ }, }, 'tot_user_signups': { - 'label': _('Total User Registration'), - 'name': 'Total User Registration', + 'label': _('Total User Registrations'), + 'name': 'Total User Registrations', 'key': 'tot_user_signups', 'field_name': 'count', 'charts': { @@ -140,8 +140,8 @@ 'fill': 'none', 'trace_type': user_signups_chart_traces, 'trace_order': user_signups_chart_order, - 'title': _('Unique RADIUS Session Count'), - 'label': _('RADIUS Session Count'), + 'title': _('Unique RADIUS Sessions'), + 'label': _('Unique RADIUS Sessions'), 'description': _( 'RADIUS Network traffic (total, download and upload).' ), @@ -183,7 +183,7 @@ 'total': 'lines', }, 'trace_order': ['total', 'download', 'upload'], - 'title': _('Total RADIUS Sessions Traffic'), + 'title': _('Traffic of RADIUS Sessions'), 'label': _('General RADIUS Traffic'), 'description': _( 'RADIUS Network traffic (total, download and upload).' @@ -219,8 +219,8 @@ 'fill': 'none', 'trace_type': user_signups_chart_traces, 'trace_order': user_signups_chart_order, - 'title': _('Unique RADIUS Session Count'), - 'label': _('General RADIUS Session Count'), + 'title': _('Unique RADIUS Sessions'), + 'label': _('General RADIUS Sessions'), 'description': _( 'RADIUS Network traffic (total, download and upload).' ), From 257c5f4351e52333477cc6f4f4ce575fa2171ee1 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 6 May 2024 22:23:43 +0530 Subject: [PATCH 42/51] [change] Hash calling_station_id --- .../integrations/monitoring/tasks.py | 17 +++-------------- .../monitoring/tests/test_metrics.py | 7 ++++--- .../integrations/monitoring/utils.py | 13 +++++++++++++ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/openwisp_radius/integrations/monitoring/tasks.py b/openwisp_radius/integrations/monitoring/tasks.py index 21936c1d..04151bf6 100644 --- a/openwisp_radius/integrations/monitoring/tasks.py +++ b/openwisp_radius/integrations/monitoring/tasks.py @@ -1,4 +1,3 @@ -import hashlib import logging from celery import shared_task @@ -8,6 +7,8 @@ from django.utils import timezone from swapper import load_model +from .utils import clean_registration_method, sha1_hash + Metric = load_model('monitoring', 'Metric') Chart = load_model('monitoring', 'Chart') RegisteredUser = load_model('openwisp_radius', 'RegisteredUser') @@ -20,18 +21,6 @@ logger = logging.getLogger(__name__) -def sha1_hash(input_string): - sha1 = hashlib.sha1() - sha1.update(input_string.encode('utf-8')) - return sha1.hexdigest() - - -def clean_registration_method(method): - if method == '': - method = 'unspecified' - return method - - def _get_user_signup_metric(organization_id, registration_method): metric, _ = Metric._get_or_create( configuration='user_signups', @@ -239,7 +228,7 @@ def post_save_radiusaccounting( extra_tags={ 'organization_id': organization_id, 'method': registration_method, - 'calling_station_id': calling_station_id, + 'calling_station_id': sha1_hash(calling_station_id), 'called_station_id': called_station_id, 'location_id': location_id, }, diff --git a/openwisp_radius/integrations/monitoring/tests/test_metrics.py b/openwisp_radius/integrations/monitoring/tests/test_metrics.py index 65c2022e..81d65dc4 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_metrics.py +++ b/openwisp_radius/integrations/monitoring/tests/test_metrics.py @@ -10,6 +10,7 @@ from openwisp_radius.tests.mixins import BaseTransactionTestCase from ..migrations import create_general_metrics +from ..utils import sha1_hash from .mixins import CreateDeviceMonitoringMixin TASK_PATH = 'openwisp_radius.integrations.monitoring.tasks' @@ -62,7 +63,7 @@ def test_post_save_radiusaccounting(self, *args): content_type=ContentType.objects.get_for_model(self.device_model), extra_tags={ 'called_station_id': device.mac_address, - 'calling_station_id': '00:00:00:00:00:00', + 'calling_station_id': sha1_hash('00:00:00:00:00:00'), 'location_id': str(device_loc.location.id), 'method': reg_user.method, 'organization_id': str(self.default_org.id), @@ -129,7 +130,7 @@ def test_post_save_radius_accounting_device_not_found(self, mocked_logger): content_type=None, extra_tags={ 'called_station_id': '11:22:33:44:55:66', - 'calling_station_id': '00:00:00:00:00:00', + 'calling_station_id': sha1_hash('00:00:00:00:00:00'), 'location_id': None, 'method': reg_user.method, 'organization_id': str(self.default_org.id), @@ -206,7 +207,7 @@ def test_post_save_radius_accounting_registereduser_not_found(self, mocked_logge content_type=ContentType.objects.get_for_model(self.device_model), extra_tags={ 'called_station_id': device.mac_address, - 'calling_station_id': '00:00:00:00:00:00', + 'calling_station_id': sha1_hash('00:00:00:00:00:00'), 'location_id': str(device_loc.location.id), 'method': 'unspecified', 'organization_id': str(self.default_org.id), diff --git a/openwisp_radius/integrations/monitoring/utils.py b/openwisp_radius/integrations/monitoring/utils.py index 6a005a28..dbbb3375 100644 --- a/openwisp_radius/integrations/monitoring/utils.py +++ b/openwisp_radius/integrations/monitoring/utils.py @@ -1,3 +1,4 @@ +import hashlib from datetime import datetime, timedelta from django.utils import timezone @@ -21,3 +22,15 @@ def get_datetime_filter_start_date(): def get_datetime_filter_stop_date(): stop_date = timezone.localdate() + timedelta(days=1) return _get_formatted_datetime_string(stop_date) + + +def sha1_hash(input_string): + sha1 = hashlib.sha1() + sha1.update(input_string.encode('utf-8')) + return sha1.hexdigest() + + +def clean_registration_method(method): + if method == '': + method = 'unspecified' + return method From 9e611c44e23302971517bfc0a30653e529e6f69d Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Tue, 7 May 2024 19:57:52 +0530 Subject: [PATCH 43/51] [req-changes] Added setting for disable org lookup for device --- docs/source/user/settings.rst | 21 +++++ docs/user/radius_monitoring.rst | 4 +- .../integrations/monitoring/settings.py | 11 +++ .../integrations/monitoring/tasks.py | 14 +-- .../monitoring/tests/test_metrics.py | 91 +++++++++++++++++++ 5 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 openwisp_radius/integrations/monitoring/settings.py diff --git a/docs/source/user/settings.rst b/docs/source/user/settings.rst index 186ed63e..4dcafae4 100644 --- a/docs/source/user/settings.rst +++ b/docs/source/user/settings.rst @@ -1044,3 +1044,24 @@ the default value is translated in other languages. If the value is customized the translations will not work, so if you need this message to be translated in different languages you should either not change the default value or prepare the additional translations. + +OpenWISP Monitoring integration related settings +================================================ + +.. note:: + + This settings are only used if you have enabled the + :ref:`integration_with_openwisp_monitoring`. + +``OPENWISP_RADIUS_MONITORING_DEVICE_LOOKUP_IGNORE_ORGANIZATION`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Default**: ``False`` + +The monitoring integration performs a database lookup for the related +device using the ``called_station_id`` attribute from the RADIUS session. +If this is set to ``True``, the integration will disable the filtering of +devices by organization of the RADIUS session. + +This is useful when multiple organizations share the same captive portal +and the device's organization and RADIUS session organization are different. diff --git a/docs/user/radius_monitoring.rst b/docs/user/radius_monitoring.rst index 082ef681..0d6ad1f9 100644 --- a/docs/user/radius_monitoring.rst +++ b/docs/user/radius_monitoring.rst @@ -1,3 +1,5 @@ +.. _integration_with_openwisp_monitoring: + Integration with OpenWISP Monitoring ------------------------------------ @@ -20,7 +22,7 @@ RADIUS metrics This chart shows number of users signed up using different registration methods. 2. Total user registrations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. image:: /images/total-user-registration-chart.png :alt: Total user registration chart diff --git a/openwisp_radius/integrations/monitoring/settings.py b/openwisp_radius/integrations/monitoring/settings.py new file mode 100644 index 00000000..0800fb93 --- /dev/null +++ b/openwisp_radius/integrations/monitoring/settings.py @@ -0,0 +1,11 @@ +from django.conf import settings + + +def get_settings_value(option, default): + return getattr(settings, f'OPENWISP_RADIUS_MONITORING_{option}', default) + + +DEVICE_LOOKUP_IGNORE_ORGANIZATION = get_settings_value( + 'DEVICE_LOOKUP_IGNORE_ORGANIZATION', + False, +) diff --git a/openwisp_radius/integrations/monitoring/tasks.py b/openwisp_radius/integrations/monitoring/tasks.py index 04151bf6..ce11b402 100644 --- a/openwisp_radius/integrations/monitoring/tasks.py +++ b/openwisp_radius/integrations/monitoring/tasks.py @@ -3,10 +3,11 @@ from celery import shared_task from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from django.db.models import Count +from django.db.models import Count, Q from django.utils import timezone from swapper import load_model +from . import settings as app_settings from .utils import clean_registration_method, sha1_hash Metric = load_model('monitoring', 'Metric') @@ -192,15 +193,16 @@ def post_save_radiusaccounting( registration_method = 'unspecified' else: registration_method = clean_registration_method(registration_method) - + device_lookup = Q(mac_address__iexact=called_station_id.replace('-', ':')) + if app_settings.DEVICE_LOOKUP_IGNORE_ORGANIZATION: + organization_id = None + else: + device_lookup &= Q(organization_id=organization_id) try: device = ( Device.objects.select_related('devicelocation') .only('id', 'devicelocation__location_id') - .get( - mac_address__iexact=called_station_id.replace('-', ':'), - organization_id=organization_id, - ) + .get(device_lookup) ) except Device.DoesNotExist: logger.warning( diff --git a/openwisp_radius/integrations/monitoring/tests/test_metrics.py b/openwisp_radius/integrations/monitoring/tests/test_metrics.py index 81d65dc4..64322e06 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_metrics.py +++ b/openwisp_radius/integrations/monitoring/tests/test_metrics.py @@ -94,6 +94,97 @@ def test_post_save_radiusaccouting_open_session(self, mocked_task): self.assertEqual(session.stop_time, None) mocked_task.assert_not_called() + @patch('logging.Logger.warning') + def test_post_save_radius_accounting_device_lookup_ignore_organization( + self, mocked_logger + ): + """ + This test ensures that the metric is written with the device's MAC address + when the OPENWISP_RADIUS_MONITORING_DEVICE_LOOKUP_IGNORE_ORGANIZATION is + set to True, even if the RadiusAccounting session and the related device + have different organizations. + """ + from .. import settings as app_settings + + user = self._create_user() + reg_user = self._create_registered_user(user=user) + org2 = self._get_org('org2') + device = self._create_device(organization=org2) + device_loc = self._create_device_location( + content_object=device, + location=self._create_location(organization=device.organization), + ) + options = _RADACCT.copy() + options.update( + { + 'unique_id': '117', + 'username': user.username, + 'called_station_id': device.mac_address.replace('-', ':').upper(), + 'calling_station_id': '00:00:00:00:00:00', + 'input_octets': '8000000000', + 'output_octets': '9000000000', + } + ) + options['stop_time'] = options['start_time'] + device_metric_qs = self.metric_model.objects.filter( + configuration='radius_acc', + name='RADIUS Accounting', + key='radius_acc', + object_id=str(device.id), + content_type=ContentType.objects.get_for_model(self.device_model), + extra_tags={ + 'called_station_id': device.mac_address, + 'calling_station_id': sha1_hash('00:00:00:00:00:00'), + 'location_id': str(device_loc.location.id), + 'method': reg_user.method, + 'organization_id': None, + }, + ) + + with self.subTest('Test DEVICE_LOOKUP_IGNORE_ORGANIZATION is set to False'): + with patch.object(app_settings, 'DEVICE_LOOKUP_IGNORE_ORGANIZATION', False): + self._create_radius_accounting(**options) + self.assertEqual( + device_metric_qs.count(), + 0, + ) + # The metric is created without the device_id + self.assertEqual( + self.metric_model.objects.filter( + configuration='radius_acc', + name='RADIUS Accounting', + key='radius_acc', + object_id=None, + content_type=None, + extra_tags={ + 'called_station_id': device.mac_address, + 'calling_station_id': sha1_hash('00:00:00:00:00:00'), + 'location_id': None, + 'method': reg_user.method, + 'organization_id': str(self.default_org.id), + }, + ).count(), + 1, + ) + + with self.subTest('Test DEVICE_LOOKUP_IGNORE_ORGANIZATION is set to True'): + with patch.object(app_settings, 'DEVICE_LOOKUP_IGNORE_ORGANIZATION', True): + options['unique_id'] = '118' + self._create_radius_accounting(**options) + self.assertEqual( + device_metric_qs.count(), + 1, + ) + metric = device_metric_qs.first() + self.assertEqual(metric.extra_tags['organization_id'], None) + traffic_chart = metric.chart_set.get(configuration='radius_traffic') + points = traffic_chart.read() + self.assertEqual(points['traces'][0][0], 'download') + self.assertEqual(points['traces'][0][1][-1], 8) + self.assertEqual(points['traces'][1][0], 'upload') + self.assertEqual(points['traces'][1][1][-1], 9) + self.assertEqual(points['summary'], {'upload': 9, 'download': 8}) + @patch('logging.Logger.warning') def test_post_save_radius_accounting_device_not_found(self, mocked_logger): """ From 6b6dbd993a512f1ece9ea750974c09014790dcb9 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Tue, 7 May 2024 22:23:22 +0530 Subject: [PATCH 44/51] [req-changes] Renamed setting --- docs/source/user/settings.rst | 4 ++-- .../integrations/monitoring/settings.py | 4 ++-- openwisp_radius/integrations/monitoring/tasks.py | 2 +- .../integrations/monitoring/tests/test_metrics.py | 14 ++++++-------- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/source/user/settings.rst b/docs/source/user/settings.rst index 4dcafae4..25f84143 100644 --- a/docs/source/user/settings.rst +++ b/docs/source/user/settings.rst @@ -1053,8 +1053,8 @@ OpenWISP Monitoring integration related settings This settings are only used if you have enabled the :ref:`integration_with_openwisp_monitoring`. -``OPENWISP_RADIUS_MONITORING_DEVICE_LOOKUP_IGNORE_ORGANIZATION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``OPENWISP_RADIUS_MONITORING_SHARED_ACCOUNTING`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Default**: ``False`` diff --git a/openwisp_radius/integrations/monitoring/settings.py b/openwisp_radius/integrations/monitoring/settings.py index 0800fb93..82f71181 100644 --- a/openwisp_radius/integrations/monitoring/settings.py +++ b/openwisp_radius/integrations/monitoring/settings.py @@ -5,7 +5,7 @@ def get_settings_value(option, default): return getattr(settings, f'OPENWISP_RADIUS_MONITORING_{option}', default) -DEVICE_LOOKUP_IGNORE_ORGANIZATION = get_settings_value( - 'DEVICE_LOOKUP_IGNORE_ORGANIZATION', +SHARED_ACCOUNTING = get_settings_value( + 'SHARED_ACCOUNTING', False, ) diff --git a/openwisp_radius/integrations/monitoring/tasks.py b/openwisp_radius/integrations/monitoring/tasks.py index ce11b402..0d6c1927 100644 --- a/openwisp_radius/integrations/monitoring/tasks.py +++ b/openwisp_radius/integrations/monitoring/tasks.py @@ -194,7 +194,7 @@ def post_save_radiusaccounting( else: registration_method = clean_registration_method(registration_method) device_lookup = Q(mac_address__iexact=called_station_id.replace('-', ':')) - if app_settings.DEVICE_LOOKUP_IGNORE_ORGANIZATION: + if app_settings.SHARED_ACCOUNTING: organization_id = None else: device_lookup &= Q(organization_id=organization_id) diff --git a/openwisp_radius/integrations/monitoring/tests/test_metrics.py b/openwisp_radius/integrations/monitoring/tests/test_metrics.py index 64322e06..b05b751d 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_metrics.py +++ b/openwisp_radius/integrations/monitoring/tests/test_metrics.py @@ -95,12 +95,10 @@ def test_post_save_radiusaccouting_open_session(self, mocked_task): mocked_task.assert_not_called() @patch('logging.Logger.warning') - def test_post_save_radius_accounting_device_lookup_ignore_organization( - self, mocked_logger - ): + def test_post_save_radius_accounting_shared_accounting(self, mocked_logger): """ This test ensures that the metric is written with the device's MAC address - when the OPENWISP_RADIUS_MONITORING_DEVICE_LOOKUP_IGNORE_ORGANIZATION is + when the OPENWISP_RADIUS_MONITORING_SHARED_ACCOUNTING is set to True, even if the RadiusAccounting session and the related device have different organizations. """ @@ -141,8 +139,8 @@ def test_post_save_radius_accounting_device_lookup_ignore_organization( }, ) - with self.subTest('Test DEVICE_LOOKUP_IGNORE_ORGANIZATION is set to False'): - with patch.object(app_settings, 'DEVICE_LOOKUP_IGNORE_ORGANIZATION', False): + with self.subTest('Test SHARED_ACCOUNTING is set to False'): + with patch.object(app_settings, 'SHARED_ACCOUNTING', False): self._create_radius_accounting(**options) self.assertEqual( device_metric_qs.count(), @@ -167,8 +165,8 @@ def test_post_save_radius_accounting_device_lookup_ignore_organization( 1, ) - with self.subTest('Test DEVICE_LOOKUP_IGNORE_ORGANIZATION is set to True'): - with patch.object(app_settings, 'DEVICE_LOOKUP_IGNORE_ORGANIZATION', True): + with self.subTest('Test SHARED_ACCOUNTING is set to True'): + with patch.object(app_settings, 'SHARED_ACCOUNTING', True): options['unique_id'] = '118' self._create_radius_accounting(**options) self.assertEqual( From b00b0ee7693249d8940164a371311af0c4e79cce Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 8 May 2024 21:35:32 +0530 Subject: [PATCH 45/51] [change] Use device.organization_id for RADIUS metric if SHARED_ACCOUNTING is enabled --- openwisp_radius/integrations/monitoring/tasks.py | 16 ++++++++++++---- .../monitoring/tests/test_metrics.py | 6 ++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/openwisp_radius/integrations/monitoring/tasks.py b/openwisp_radius/integrations/monitoring/tasks.py index 0d6c1927..b6f271e0 100644 --- a/openwisp_radius/integrations/monitoring/tasks.py +++ b/openwisp_radius/integrations/monitoring/tasks.py @@ -194,14 +194,13 @@ def post_save_radiusaccounting( else: registration_method = clean_registration_method(registration_method) device_lookup = Q(mac_address__iexact=called_station_id.replace('-', ':')) - if app_settings.SHARED_ACCOUNTING: - organization_id = None - else: + # Do not use organization_id for device lookup if shared accounting is enabled + if not app_settings.SHARED_ACCOUNTING: device_lookup &= Q(organization_id=organization_id) try: device = ( Device.objects.select_related('devicelocation') - .only('id', 'devicelocation__location_id') + .only('id', 'organization_id', 'devicelocation__location_id') .get(device_lookup) ) except Device.DoesNotExist: @@ -213,6 +212,8 @@ def post_save_radiusaccounting( object_id = None content_type = None location_id = None + if app_settings.SHARED_ACCOUNTING: + organization_id = None else: object_id = str(device.id) content_type = ContentType.objects.get_for_model(Device) @@ -220,6 +221,13 @@ def post_save_radiusaccounting( location_id = str(device.devicelocation.location_id) else: location_id = None + # Give preference to the organization_id of the device + # over RadiusAccounting object. + # This would also handle the case when SHARED_ACCOUNTING is enabled, + # and write the metric with the same organization_id as the device. + # If SHARED_ACCOUNTING is disabled, the organization_id of + # Device and RadiusAccounting would be same. + organization_id = str(device.organization_id) metric, created = Metric._get_or_create( configuration='radius_acc', diff --git a/openwisp_radius/integrations/monitoring/tests/test_metrics.py b/openwisp_radius/integrations/monitoring/tests/test_metrics.py index b05b751d..c2317af6 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_metrics.py +++ b/openwisp_radius/integrations/monitoring/tests/test_metrics.py @@ -135,7 +135,7 @@ def test_post_save_radius_accounting_shared_accounting(self, mocked_logger): 'calling_station_id': sha1_hash('00:00:00:00:00:00'), 'location_id': str(device_loc.location.id), 'method': reg_user.method, - 'organization_id': None, + 'organization_id': str(device.organization_id), }, ) @@ -174,7 +174,9 @@ def test_post_save_radius_accounting_shared_accounting(self, mocked_logger): 1, ) metric = device_metric_qs.first() - self.assertEqual(metric.extra_tags['organization_id'], None) + self.assertEqual( + metric.extra_tags['organization_id'], str(device.organization_id) + ) traffic_chart = metric.chart_set.get(configuration='radius_traffic') points = traffic_chart.read() self.assertEqual(points['traces'][0][0], 'download') From b360628d642579fe435991abb8aa404d70e0db36 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 8 May 2024 22:49:36 +0530 Subject: [PATCH 46/51] [req-change] Don't overwrite the organization of RadiusAccounting --- .gitignore | 2 ++ openwisp_radius/integrations/monitoring/tasks.py | 9 ++------- .../integrations/monitoring/tests/test_metrics.py | 4 ++-- setup.cfg | 2 ++ 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index d2241941..0f91b06b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +tests/openwisp2/saml/* + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/openwisp_radius/integrations/monitoring/tasks.py b/openwisp_radius/integrations/monitoring/tasks.py index b6f271e0..f8aef603 100644 --- a/openwisp_radius/integrations/monitoring/tasks.py +++ b/openwisp_radius/integrations/monitoring/tasks.py @@ -180,6 +180,7 @@ def post_save_radiusaccounting( output_octets, calling_station_id, called_station_id, + time=None, ): try: registration_method = ( @@ -221,13 +222,6 @@ def post_save_radiusaccounting( location_id = str(device.devicelocation.location_id) else: location_id = None - # Give preference to the organization_id of the device - # over RadiusAccounting object. - # This would also handle the case when SHARED_ACCOUNTING is enabled, - # and write the metric with the same organization_id as the device. - # If SHARED_ACCOUNTING is disabled, the organization_id of - # Device and RadiusAccounting would be same. - organization_id = str(device.organization_id) metric, created = Metric._get_or_create( configuration='radius_acc', @@ -249,6 +243,7 @@ def post_save_radiusaccounting( 'output_octets': output_octets, 'username': sha1_hash(username), }, + time=time, ) if not object_id: # Adding a chart requires all parameters of extra_tags to be present. diff --git a/openwisp_radius/integrations/monitoring/tests/test_metrics.py b/openwisp_radius/integrations/monitoring/tests/test_metrics.py index c2317af6..336c5167 100644 --- a/openwisp_radius/integrations/monitoring/tests/test_metrics.py +++ b/openwisp_radius/integrations/monitoring/tests/test_metrics.py @@ -135,7 +135,7 @@ def test_post_save_radius_accounting_shared_accounting(self, mocked_logger): 'calling_station_id': sha1_hash('00:00:00:00:00:00'), 'location_id': str(device_loc.location.id), 'method': reg_user.method, - 'organization_id': str(device.organization_id), + 'organization_id': str(self.default_org.id), }, ) @@ -175,7 +175,7 @@ def test_post_save_radius_accounting_shared_accounting(self, mocked_logger): ) metric = device_metric_qs.first() self.assertEqual( - metric.extra_tags['organization_id'], str(device.organization_id) + metric.extra_tags['organization_id'], str(self.default_org.id) ) traffic_chart = metric.chart_set.get(configuration='radius_traffic') points = traffic_chart.read() diff --git a/setup.cfg b/setup.cfg index 73f56700..b509a271 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,8 @@ universal=1 ignore = W605, W503, W504, E203 exclude = *.egg-info, .git, + ./tests/openwisp2/saml/* + ./test-* ./tests/*settings*.py, docs/* max-line-length = 88 From b02c43bc928a14245db345f3a6c0d49b134609ce Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 13 May 2024 15:42:55 +0530 Subject: [PATCH 47/51] [chores] Removed changes to .gitignore and setup.cfg --- .gitignore | 2 -- setup.cfg | 2 -- 2 files changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 0f91b06b..d2241941 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -tests/openwisp2/saml/* - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/setup.cfg b/setup.cfg index b509a271..73f56700 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,8 +9,6 @@ universal=1 ignore = W605, W503, W504, E203 exclude = *.egg-info, .git, - ./tests/openwisp2/saml/* - ./test-* ./tests/*settings*.py, docs/* max-line-length = 88 From edeaa796d405b9e57740d8ebe3291ef95ce2598b Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 30 Oct 2024 21:10:04 +0530 Subject: [PATCH 48/51] [req-changes] Updated docs --- docs/user/radius_monitoring.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/user/radius_monitoring.rst b/docs/user/radius_monitoring.rst index 0d6ad1f9..9838456e 100644 --- a/docs/user/radius_monitoring.rst +++ b/docs/user/radius_monitoring.rst @@ -58,12 +58,7 @@ in ``INSTALLED_APPS`` of your Django project's settings as following: # In your_project/settings.py - INSTALLED_APPS = [ - # ... - 'openwisp_radius', - 'openwisp_radius.integrations.monitoring' # <--- add the app after openwisp_radius - # ... - ] + INSTALLED_APPS.append('openwisp_radius.integrations.monitoring') .. note:: From 520efe273df087172a285244b91c5ab19cb1eb7c Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 30 Oct 2024 21:32:02 +0530 Subject: [PATCH 49/51] [deps] Updated requirements-test.txt --- .../images/radius-dashboard-charts.png | Bin docs/source/index.rst | 70 -- docs/source/user/settings.rst | 1067 ----------------- docs/user/radius_monitoring.rst | 65 +- requirements-test.txt | 2 +- 5 files changed, 34 insertions(+), 1170 deletions(-) rename docs/{source => }/images/radius-dashboard-charts.png (100%) delete mode 100644 docs/source/index.rst delete mode 100644 docs/source/user/settings.rst diff --git a/docs/source/images/radius-dashboard-charts.png b/docs/images/radius-dashboard-charts.png similarity index 100% rename from docs/source/images/radius-dashboard-charts.png rename to docs/images/radius-dashboard-charts.png diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 3032fd94..00000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,70 +0,0 @@ -=============== -openwisp-radius -=============== - -.. image:: https://travis-ci.org/openwisp/openwisp-radius.svg?branch=master - :target: https://travis-ci.org/openwisp/openwisp-radius - :alt: CI build status - -.. image:: https://coveralls.io/repos/github/openwisp/openwisp-radius/badge.svg?branch=master - :target: https://coveralls.io/github/openwisp/openwisp-radius?branch=master - :alt: Test Coverage - -.. image:: https://img.shields.io/librariesio/release/github/openwisp/openwisp-radius - :target: https://libraries.io/github/openwisp/openwisp-radius#repository_dependencies - :alt: Dependency monitoring - -.. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg - :target: https://gitter.im/openwisp/general - :alt: Chat - -.. image:: https://badge.fury.io/py/openwisp-radius.svg - :target: http://badge.fury.io/py/openwisp-radius - :alt: Pypi Version - -.. image:: https://pepy.tech/badge/openwisp-radius - :target: https://pepy.tech/project/openwisp-radius - :alt: Downloads - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://pypi.org/project/black/ - :alt: code style: black - -**OpenWISP-RADIUS** provides an admin interface to a -`freeradius `_ database and offers features -that are common in WiFi and ISP deployments. - -**Need a quick overview?** `Try the OpenWISP Demo `_. - -.. note:: - If you're building a public wifi service, we suggest - to take a look at `openwisp-wifi-login-pages `_, - which is built to work with openwisp-radius. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp2-docs/master/assets/design/openwisp-logo-black.svg - :target: http://openwisp.org - -.. toctree:: - :maxdepth: 2 - - /developer/setup - /developer/freeradius - /developer/freeradius_wpa_enterprise - /user/settings - /user/management_commands - /user/importing_users - /user/generating_users - /user/enforcing_limits - /user/registration - /user/social_login - /user/saml - /user/radius_monitoring - /user/change_of_authorization - /user/api - /developer/signals - /developer/how_to_extend - /developer/captive_portal_mock.rst - /general/support - /developer/contributing - /general/goals - /general/changelog.rst diff --git a/docs/source/user/settings.rst b/docs/source/user/settings.rst deleted file mode 100644 index 25f84143..00000000 --- a/docs/source/user/settings.rst +++ /dev/null @@ -1,1067 +0,0 @@ -Available settings ------------------- - -Admin related settings -====================== - -These settings control details of the administration interface of openwisp-radius. - -.. note:: - - The values of overridden settings fields do not change even when - the global defaults are changed. - -``OPENWISP_RADIUS_EDITABLE_ACCOUNTING`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Whether ``radacct`` entries are editable from the django admin or not. - -``OPENWISP_RADIUS_EDITABLE_POSTAUTH`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Whether ``postauth`` logs are editable from the django admin or not. - -``OPENWISP_RADIUS_GROUPCHECK_ADMIN`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Direct editing of group checks items is disabled by default because -these can be edited through inline items in the Radius Group -admin (Freeradius > Groups). - -*This is done with the aim of simplifying the admin interface and avoid -overwhelming users with too many options*. - -If for some reason you need to enable direct editing of group checks -you can do so by setting this to ``True``. - -``OPENWISP_RADIUS_GROUPREPLY_ADMIN`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Direct editing of group reply items is disabled by default because -these can be edited through inline items in the Radius Group -admin (Freeradius > Groups). - -*This is done with the aim of simplifying the admin interface and avoid -overwhelming users with too many options*. - -If for some reason you need to enable direct editing of group replies -you can do so by setting this to ``True``. - -``OPENWISP_RADIUS_USERGROUP_ADMIN`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Direct editing of user group items (``radusergroup``) is disabled by default -because these can be edited through inline items in the User -admin (Users and Organizations > Users). - -*This is done with the aim of simplifying the admin interface and avoid -overwhelming users with too many options*. - -If for some reason you need to enable direct editing of user group items -you can do so by setting this to ``True``. - -``OPENWISP_RADIUS_USER_ADMIN_RADIUSTOKEN_INLINE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -The functionality of editing a user's ``RadiusToken`` directly -through an inline from the user admin page is disabled by default. - -*This is done with the aim of simplifying the admin interface and avoid -overwhelming users with too many options*. - -If for some reason you need to enable editing user's ``RadiusToken`` -from the user admin page, you can do so by setting this to ``True``. - -Model related settings -====================== - -These settings control details of the openwisp-radius model classes. - -``OPENWISP_RADIUS_DEFAULT_SECRET_FORMAT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``NT-Password`` - -The default encryption format for storing radius check values. - -``OPENWISP_RADIUS_DISABLED_SECRET_FORMATS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``[]`` - -A list of disabled encryption formats, by default all formats are -enabled in order to keep backward compatibility with legacy systems. - -``OPENWISP_RADIUS_BATCH_DEFAULT_PASSWORD_LENGTH`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``8`` - -The default password length of the auto generated passwords while -batch addition of users from the csv. - -``OPENWISP_RADIUS_BATCH_DELETE_EXPIRED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``18`` - -It is the number of months after which the expired users are deleted. - -``OPENWISP_RADIUS_BATCH_PDF_TEMPLATE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It is the template used to generate the pdf when users are being generated using the batch add users feature using the prefix. - -The value should be the absolute path to the template of the pdf. - -``OPENWISP_RADIUS_EXTRA_NAS_TYPES`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``tuple()`` - -This setting can be used to add custom NAS types that can be used from the -admin interface when managing NAS instances. - -For example, you want a custom NAS type called ``cisco``, you would add -the following to your project ``settings.py``: - -.. code-block:: python - - OPENWISP_RADIUS_EXTRA_NAS_TYPES = ( - ('cisco', 'Cisco Router'), - ) - -.. _openwisp_radius_freeradius_allowed_hosts: - -``OPENWISP_RADIUS_FREERADIUS_ALLOWED_HOSTS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``[]`` - -List of host IP addresses or subnets allowed to consume the freeradius -API endpoints (Authorize, Accounting and Postauth), i.e the value -of this option should be the IP address of your freeradius -instance. Example: If your freeradius instance is running on -the same host machine as OpenWISP, the value should be ``127.0.0.1``. -Similarly, if your freeradius instance is on a different host in -the private network, the value should be the private IP of freeradius -host like ``192.0.2.50``. If your freeradius is on a public network, -please use the public IP of your freeradius instance. - -You can use subnets when freeradius is hosted on a variable IP, eg: - -- ``198.168.0.0/24`` to allow the entire LAN. -- ``0.0.0.0/0`` to allow any address (useful for development / testing). - -This value can be overridden per organization in the organization -change page. You can skip setting this option if you intend to set -it from organization change page for each organization. - -.. image:: /images/freeradius_allowed_hosts.png - :alt: Organization change page freeradius settings - -.. code-block:: python - - OPENWISP_RADIUS_FREERADIUS_ALLOWED_HOSTS = ['127.0.0.1', '192.0.2.10', '192.168.0.0/24'] - -If this option and organization change page option are both -empty, then all freeradius API requests for the organization -will return ``403``. - -.. _coa_enabled_setting: - -``OPENWISP_RADIUS_COA_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False``` - -If set to ``True``, openwisp-radius will update the NAS with the -user's current RADIUS attributes whenever the ``RadiusGroup`` of -user is changed. This allow enforcing of rate limits on active -RADIUS sessions without requiring users to re-authenticate. For -more details, :ref:`read the dedicated section for configuring -openwisp-radius and NAS for using CoA `. - -This can be overridden for each organization separately -via the organization radius settings section of the admin interface. - -.. image:: /images/organization_coa_enabled.png - :alt: CoA enabled - -```RADCLIENT_ATTRIBUTE_DICTIONARIES``` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------+ -| **type**: | ``list`` | -+--------------+----------+ -| **default**: | ``[]`` | -+--------------+----------+ - -List of absolute file paths of additional RADIUS dictionaries used -for RADIUS attribute mapping. - -.. note:: - - A `default dictionary `_ - is shipped with openwisp-radius. Any dictionary added using this setting - will be used alongside the default dictionary. - -``OPENWISP_RADIUS_MAX_CSV_FILE_SIZE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------------------------+ -| **type**: | ``int`` | -+--------------+----------------------------+ -| **default**: | `5 * 1024 * 1024` (5 MB) | -+--------------+----------------------------+ - -This setting can be used to set the maximum size limit for firmware images, eg: - -.. code-block:: python - - OPENWISP_RADIUS_MAX_CSV_FILE_SIZE = 10 * 1024 * 1024 # 10MB - -.. note:: - - The numeric value represents the size of files in bytes. - Setting this to ``None`` will mean there's no max size. - -``OPENWISP_RADIUS_PRIVATE_STORAGE_INSTANCE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+--------------+-------------------------------------------------------------------------------------+ -| **type**: | ``str`` | -+--------------+-------------------------------------------------------------------------------------+ -| **default**: | ``openwisp_radius.private_storage.storage.private_file_system_storage`` | -+--------------+-------------------------------------------------------------------------------------+ - -Dotted path to an instance of any one of the storage classes in -`private_storage `_. -This instance is used for storing csv files of batch imports of users. - -By default, an instance of ``private_storage.storage.files.PrivateFileSystemStorage`` -is used. - -.. _openwisp_radius_called_station_ids: - -``OPENWISP_RADIUS_CALLED_STATION_IDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``{}`` - -This setting allows to specify the parameters to connect to the different -OpenVPN management interfaces available for an organization. This setting is used by the -:ref:`convert_called_station_id ` command. - -It should contain configuration in following format: - -.. code-block:: python - - OPENWISP_RADIUS_CALLED_STATION_IDS = { - # UUID of the organization for which settings are being specified - # In this example 'default' - '': { - 'openvpn_config': [ - { - # Host address of OpenVPN management - 'host': '', - # Port of OpenVPN management interface. Defaults to 7505 (integer) - 'port': 7506, - # Password of OpenVPN management interface (optional) - 'password': '', - } - ], - # List of CALLED STATION IDs that has to be converted, - # These look like: 00:27:22:F3:FA:F1:gw1.openwisp.org - 'unconverted_ids': [''], - } - } - -``OPENWISP_RADIUS_CONVERT_CALLED_STATION_ON_CREATE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -If set to ``True``, "Called Station ID" of a RADIUS session will be -converted (as per configuration defined in :ref:`OPENWISP_RADIUS_CALLED_STATION_IDS `) -just after the RADIUS session is created. - -.. _openwisp_radius_openvpn_datetime_format: - -``OPENWISP_RADIUS_OPENVPN_DATETIME_FORMAT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``u'%a %b %d %H:%M:%S %Y'`` - -Specifies the datetime format of OpenVPN management status parser used by the -:ref:`convert_called_station_id ` -command. - -``OPENWISP_RADIUS_UNVERIFY_INACTIVE_USERS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``0`` (disabled) - -Number of days from user's ``last_login`` after which the -user will be flagged as *unverified*. - -When set to ``0``, the feature would be disabled and the user will -not be flagged as *unverified*. - -``OPENWISP_RADIUS_DELETE_INACTIVE_USERS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``0`` (disabled) - -Number of days from user's ``last_login`` after which the -user will be deleted. - -When set to ``0``, the feature would be disabled and the user will -not be deleted. - - -API and user token related settings -=================================== - -These settings control details related to the API and the radius user token. - -``OPENWISP_RADIUS_API_URLCONF`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``None`` - -Changes the urlconf option of django urls to point the RADIUS API -urls to another installed module, example, ``myapp.urls`` -(useful when you have a seperate API instance.) - -``OPENWISP_RADIUS_API_BASEURL`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``/`` (points to same server) - -If you have a seperate instance of openwisp-radius API on a -different domain, you can use this option to change the base of the image -download URL, this will enable you to point to your API server's domain, -example value: ``https://myradius.myapp.com``. - -.. _openwisp_radius_api: - -``OPENWISP_RADIUS_API`` -~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``True`` - -Indicates whether the REST API of openwisp-radius is enabled or not. - -``OPENWISP_RADIUS_DISPOSABLE_RADIUS_USER_TOKEN`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``True`` - -Radius user tokens are used for authorizing users. - -When this setting is ``True`` radius user tokens are deleted right after a successful -authorization is performed. This reduces the possibility of attackers reusing -the access tokens and posing as other users if they manage to intercept it somehow. - -.. _openwisp_radius_api_authorize_reject: - -``OPENWISP_RADIUS_API_AUTHORIZE_REJECT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Indicates wether the :ref:`Authorize API view ` will return -``{"control:Auth-Type": "Reject"}`` or not. - -Rejecting an authorization request explicitly will prevent freeradius from -attempting to perform authorization with other mechanisms (eg: radius checks, LDAP, etc.). - -When set to ``False``, if an authorization request fails, the API will respond with -``None``, which will allow freeradius to keep attempting to authorize the request -with other freeradius modules. - -Set this to ``True`` if you are performing authorization exclusively through the REST API. - -``OPENWISP_RADIUS_API_ACCOUNTING_AUTO_GROUP`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``True`` - -When this setting is enabled, every accounting instance saved from the API will have -its ``groupname`` attribute automatically filled in. -The value filled in will be the ``groupname`` of the ``RadiusUserGroup`` of the highest -priority among the RadiusUserGroups related to the user with the ``username`` as in the -accounting instance. -In the event there is no user in the database corresponding to the ``username`` in the -accounting instance, the failure will be logged with ``warning`` level but the accounting -will be saved as usual. - -.. _openwisp_radius_allowed_mobile_prefixes: - -``OPENWISP_RADIUS_ALLOWED_MOBILE_PREFIXES`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``[]`` - -This setting is used to specify a list of international mobile prefixes which should -be allowed to register into the system via the :ref:`user registration API `. - -That is, only users with phone numbers using the specified international prefixes will -be allowed to register. - -Leaving this unset or setting it to an empty list (``[]``) will effectively allow -any international mobile prefix to register (which is the default setting). - -For example: - -.. code-block:: python - - OPENWISP_RADIUS_ALLOWED_MOBILE_PREFIXES = ['+44', '+237'] - -Using the setting above will only allow phone numbers from the UK (``+44``) -or Cameroon (``+237``). - -.. note:: - - This setting is applicable only for organizations - which have :ref:`enabled the SMS verification option - `. - -``OPENWISP_RADIUS_ALLOW_FIXED_LINE_OR_MOBILE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -OpenWISP RADIUS only allow using mobile phone numbers for user registration. -This can cause issues in regions where fixed line and mobile phone numbers -uses the same pattern (e.g. USA). Setting the value to ``True`` -would make phone number type checking less strict. - -.. _openwisp_radius_optional_registration_fields: - -``OPENWISP_RADIUS_OPTIONAL_REGISTRATION_FIELDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: - -.. code-block:: python - - { - 'first_name': 'disabled', - 'last_name': 'disabled', - 'birth_date': 'disabled', - 'location': 'disabled', - } - -This global setting is used to specify if the optional user fields -(``first_name``, ``last_name``, ``location`` and ``birth_date``) -shall be disabled (hence ignored), allowed or required in the -:ref:`User Registration API `. - -The allowed values are: - -- ``disabled``: (**default**) the field is disabled. -- ``allowed``: the field is allowed but not mandatory. -- ``mandatory``: the field is mandatory. - -For example: - -.. code-block:: python - - OPENWISP_RADIUS_OPTIONAL_REGISTRATION_FIELDS = { - 'first_name': 'disabled', - 'last_name': 'disabled', - 'birth_date': 'mandatory', - 'location': 'allowed', - } - -Means: - -- ``first_name`` and ``last_name`` fields are not required and their values - if provided are ignored. -- ``location`` field is not required but its value will - be saved to the database if provided. -- ``birth_date`` field is required and a ``ValidationError`` - exception is raised if its value is not provided. - -The setting for each field can also be overridden at organization level -if needed, by going to -``Home › Users and Organizations › Organizations > Edit organization`` and -then scrolling down to ``ORGANIZATION RADIUS SETTINGS``. - -.. image:: /images/optional_fields.png - :alt: optional field setting - -By default the fields at organization level hold a ``NULL`` value, -which means that the global setting specified in ``settings.py`` will -be used. - -``OPENWISP_RADIUS_PASSWORD_RESET_URLS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. note:: - - This setting can be overridden for each organization in the - organization admin page, the setting implementation is left - for backward compatibility but may be deprecated in the future. - -**Default**: - -.. code-block:: python - - { - '__all__': 'https://{site}/{organization}/password/reset/confirm/{uid}/{token}' - } - -A dictionary representing the frontend URLs through which end users can complete -the password reset operation. - -The frontend could be `openwisp-wifi-login-pages `_ -or another in-house captive page solution. - -Keys of the dictionary must be either UUID of organizations or ``__all__``, which is the fallback URL -that will be used in case there's no customized URL for a specific organization. - -The password reset URL must contain the "{token}" and "{uid}" placeholders. - -The meaning of the variables in the string is the following: - -- ``{site}``: site domain as defined in the - `django site framework `_ - (defaults to example.com and an be changed through the django admin) -- ``{organization}``: organization slug -- ``{uid}``: uid of the password reset request -- ``{token}``: token of the password reset request - -If you're using `openwisp-wifi-login-pages `_, -the configuration is fairly simple, in case the nodejs app is installed in the same domain -of openwisp-radius, you only have to ensure the domain field in the main Site object is correct, -if instead the nodejs app is deployed on a different domain, say ``login.wifiservice.com``, -the configuration should be simply changed to: - -.. code-block:: python - - { - '__all__': 'https://login.wifiservice.com/{organization}/password/reset/confirm/{uid}/{token}' - } - -.. _openwisp_radius_registration_api_enabled: - -``OPENWISP_RADIUS_REGISTRATION_API_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``True`` - -Indicates whether the API registration view is enabled or not. -When this setting is disabled (i.e. ``False``), the registration API view is disabled. - -**This setting can be overridden in individual organizations -via the admin interface**, by going to *Organizations* -then edit a specific organization and scroll down to -*"Organization RADIUS settings"*, as shown in the screenshot below. - -.. image:: /images/organization_registration_setting.png - :alt: Organization RADIUS settings - -.. note:: - - We recommend using the override via the admin interface only when there - are special organizations which need a different configuration, otherwise, - if all the organization use the same configuration, we recommend - changing the global setting. - -.. _openwisp_radius_sms_verification_enabled: - -``OPENWISP_RADIUS_SMS_VERIFICATION_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -.. note:: - - If you're looking for instructions on how to configure SMS sending, - see :ref:`SMS Token Related Settings `. - -If :ref:`Identity verification is required `, -this setting indicates whether users who sign up should be required to -verify their mobile phone number via SMS. - -This can be overridden for each organization separately -via the organization radius settings section of the admin interface. - -.. image:: /images/organization_sms_verification_setting.png - :alt: SMS verification enabled - -.. _openwisp_radius_needs_identity_verification: - -``OPENWISP_RADIUS_MAC_ADDR_ROAMING_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Indicates whether MAC address roaming is supported. -When this setting is enabled (i.e. ``True``), -MAC address roaming is enabled for all organizations. - -**This setting can be overridden in individual organizations -via the admin interface**, by going to *Organizations* -then edit a specific organization and scroll down to -*"Organization RADIUS settings"*, as shown in the screenshot below. - -.. image:: /images/mac-address-roaming.png - :alt: Organization MAC Address Roaming settings - -.. note:: - - We recommend using the override via the admin interface only when there - are special organizations which need a different configuration, otherwise, - if all the organization use the same configuration, we recommend - changing the global setting. - -``OPENWISP_RADIUS_NEEDS_IDENTITY_VERIFICATION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Indicates whether organizations require a user to be verified in order to login. -This can be overridden globally or for each organization separately via the admin -interface. - -If this is enabled, each registered user should be verified using a verification method. -The following choices are available by default: - -- ``''`` (empty string): unspecified -- ``manual``: Manually created -- ``email``: Email -- ``mobile_phone``: Mobile phone number - :ref:`verification via SMS ` -- ``social_login``: :ref:`social login feature ` - -.. note:: - - Of the methods listed above, ``mobile_phone`` is generally - accepted as a legal and valid form of indirect identity verification - in those countries who require to provide - a valid ID document before buying a SIM card. - - Organizations which are required by law to identify their users - before allowing them to access the network (eg: ISPs) can restrict - users to register only through this method and can configure the system - to only :ref:`allow international mobile prefixes ` - of countries which require a valid ID document to buy a SIM card. - - **Disclaimer:** these are just suggestions on possible configurations - of OpenWISP RADIUS and must not be considered as legal advice. - -.. _register_registration_method: - -Adding support for more registration/verification methods -######################################################### - -For those who need to implement additional registration and identity -verification methods, such as supporting a National ID card, new methods -can be added or an existing method can be removed using -the ``register_registration_method`` -and ``unregister_registration_method`` functions respectively. - -For example: - -.. code-block:: python - - from openwisp_radius.registration import ( - register_registration_method, - unregister_registration_method, - ) - - # Enable registering via national digital ID - register_registration_method('national_id', 'National Digital ID') - - # Remove mobile verification method - unregister_registration_method('mobile_phone') - -.. note:: - - Both functions will fail if a specific registration method - is already registered or unregistered, unless the keyword argument - ``fail_loud`` is passed as ``False`` (this useful when working with - additional registration methods which are supported by multiple - custom modules). - - Pass ``strong_identity`` as ``True`` to to indicate that users who - register using that method have indirectly verified their identity - (eg: :ref:`SMS verification - `, - credit card, national ID card, etc). - -.. warning:: - - If you need to implement a registration method that needs to grant limited - internet access to unverified users so they can complete their - verification process online on other websites which cannot be predicted - and hence cannot be added to the walled garden, you can pass - ``authorize_unverified=True`` to the ``register_registration_method`` - function. - - This is needed to implement payment flows in which users insert - a specific 3D secure code in the website of their bank. - Keep in mind that you should create a specific limited radius group - for these unverified users. - - Payment flows and credit/debit card verification are fully implemented - in **OpenWISP Subscriptions**, a premium module available only to - customers of the - :ref:`commercial support offering of OpenWISP `. - -Email related settings -====================== - -Emails can be sent to users whose usernames or passwords have been auto-generated. -The content of these emails can be customized with the settings explained below. - -.. _openwisp_radius_batch_mail_subject: - -``OPENWISP_RADIUS_BATCH_MAIL_SUBJECT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``Credentials`` - -It is the subject of the mail to be sent to the users. Eg: ``Login Credentials``. - -.. _openwisp_radius_batch_mail_message: - -``OPENWISP_RADIUS_BATCH_MAIL_MESSAGE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``username: {}, password: {}`` - -The message should be a string in the format ``Your username is {} and password is {}``. - -The text could be anything but should have the format string operator ``{}`` for -``.format`` operations to work. - -.. _openwisp_radius_batch_mail_sender: - -``OPENWISP_RADIUS_BATCH_MAIL_SENDER`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``settings.DEFAULT_FROM_EMAIL`` - -It is the sender email which is also to be configured in the SMTP settings. -The default sender email is a common setting from the -`Django core settings `_ -under ``DEFAULT_FROM_EMAIL``. -Currently, ``DEFAULT_FROM_EMAIL`` is set to to ``webmaster@localhost``. - -.. _counter_related_settings: - -Counter related settings -======================== - -.. _counters_setting: - -``OPENWISP_RADIUS_COUNTERS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: depends on the database backend in use, -see :ref:`counters` to find out what are the default counters enabled. - -It's a list of strings, each representing the python path to a counter class. - -It may be set to an empty list or tuple to disable the counter feature, eg: - -.. code-block:: python - - OPENWISP_RADIUS_COUNTERS = [] - -If custom counters have been implemented, this setting should be changed -to include the new classes, eg: - -.. code-block:: python - - OPENWISP_RADIUS_COUNTERS = [ - # default counters for PostgreSQL, may be removed if not needed - 'openwisp_radius.counters.postgresql.daily_counter.DailyCounter', - 'openwisp_radius.counters.postgresql.daily_traffic_counter.DailyTrafficCounter', - # custom counters - 'myproject.counters.CustomCounter1', - 'myproject.counters.CustomCounter2', - ] - -.. _traffic_counter_check_name: - -``OPENWISP_RADIUS_TRAFFIC_COUNTER_CHECK_NAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``Max-Daily-Session-Traffic`` - -Used by :ref:`daily_traffic_counter`, -it indicates the check attribute which is looked for -in the database to find the maximum amount of daily traffic -which users having the default ``users`` radius group assigned can consume. - -.. _traffic_counter_reply_name: - -``OPENWISP_RADIUS_TRAFFIC_COUNTER_REPLY_NAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``CoovaChilli-Max-Total-Octets`` - -Used by :ref:`daily_traffic_counter`, -it indicates the reply attribute which is returned to the NAS -to indicate how much remaining traffic users -which users having the default ``users`` radius group assigned -can consume. - -It should be changed according to the NAS software in use, for example, -if using PfSense, this setting should be set to ``pfSense-Max-Total-Octets``. - -``OPENWISP_RADIUS_RADIUS_ATTRIBUTES_TYPE_MAP`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``{}`` - -Used by :ref:`User Radius Usage API `, -it stores mapping of RADIUS attributes to the unit of value -enforced by the attribute, e.g. ``bytes`` for traffic counters and -``seconds`` for session time counters. - -In the following example, the setting is configured to return ``bytes`` -type in the API response for ``ChilliSpot-Max-Input-Octets`` attribute: - -.. code-block:: python - - OPENWISP_RADIUS_RADIUS_ATTRIBUTES_TYPE_MAP = { - 'ChilliSpot-Max-Input-Octets': 'bytes' - } - -.. _social_login_settings: - -Social Login related settings -============================= - -The following settings are related to the :ref:`social login feature `. - -.. _openwisp_radius_social_registration_enabled: - -``OPENWISP_RADIUS_SOCIAL_REGISTRATION_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Indicates whether the registration using social applications -is enabled or not. When this setting is enabled (i.e. ``True``), -authentication using social applications is enabled for all organizations. - -**This setting can be overridden in individual organizations -via the admin interface**, by going to *Organizations* -then edit a specific organization and scroll down to -*"Organization RADIUS settings"*, as shown in the screenshot below. - -.. image:: /images/organization_social_login_setting.png - :alt: Organization social login settings - -.. note:: - - We recommend using the override via the admin interface only when there - are special organizations which need a different configuration, otherwise, - if all the organization use the same configuration, we recommend - changing the global setting. - -.. _saml_settings: - -SAML related settings -===================== - -The following settings are related to the :ref:`SAML feature `. - -.. _openwisp_radius_saml_registration_enabled: - -``OPENWISP_RADIUS_SAML_REGISTRATION_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Indicates whether registration using SAML is enabled or not. -When this setting is enabled (i.e. ``True``), -authentication using SAML is enabled for all organizations. - -**This setting can be overridden in individual organizations -via the admin interface**, by going to *Organizations* -then edit a specific organization and scroll down to -*"Organization RADIUS settings"*, as shown in the screenshot below. - -.. image:: /images/organization_saml_setting.png - :alt: Organization SAML settings - -.. note:: - - We recommend using the override via the admin interface only when there - are special organizations which need a different configuration, otherwise, - if all the organization use the same configuration, we recommend - changing the global setting. - -``OPENWISP_RADIUS_SAML_REGISTRATION_METHOD_LABEL`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``'Single Sign-On (SAML)'`` - -Sets the verbose name of SAML registration method. - -``OPENWISP_RADIUS_SAML_IS_VERIFIED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Setting this to ``True`` will automatically flag user accounts -created during SAML sign-in as verified users (``RegisteredUser.is_verified=True``). - -This is useful when SAML identity providers can be trusted -to be legally valid identity verifiers. - -.. _openwisp_radius_saml_updates_pre_existing_username: - -``OPENWISP_RADIUS_SAML_UPDATES_PRE_EXISTING_USERNAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -Allows updating username of a registered user with the value -received from SAML Identity Provider. Read the -:ref:`FAQs in SAML integration documentation ` -for details. - -.. _sms_token_related_settings: - -SMS token related settings -========================== - -These settings allow to control aspects and limitations of the SMS tokens -which are sent to users for the purpose of -:ref:`verifying their mobile phone number -`. - -These settings are applicable only when -:ref:`SMS verification is enabled `. - -``SENDSMS_BACKEND`` -~~~~~~~~~~~~~~~~~~~ - -This setting takes a python path which points to the `django-sendsms -`__ -backend which will be used by the system to send SMS messages. - -The list of supported SMS services can be seen in the source code of -`the django-sendsms backends -`__. -Adding support for other SMS services can be done by subclassing -the ``BaseSmsBackend`` and implement the logic needed to talk to the -SMS service. - -The value of this setting can point to any class on the python path, -so the backend doesn't have to be necessarily shipped in django-sendsms -but can be deployed in any other location. - -``OPENWISP_RADIUS_SMS_TOKEN_DEFAULT_VALIDITY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``30`` - -For how many minutes the SMS token is valid for. - -``OPENWISP_RADIUS_SMS_TOKEN_LENGTH`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``6`` - -The length of the SMS token. - -``OPENWISP_RADIUS_SMS_TOKEN_HASH_ALGORITHM`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``'sha256'`` - -The hashing algorithm used to generate the numeric code. - -``OPENWISP_RADIUS_SMS_COOLDOWN`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``30`` - -Seconds users needs to wait before being able to request a new SMS token. - -``OPENWISP_RADIUS_SMS_TOKEN_MAX_ATTEMPTS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``5`` - -The max number of mistakes tolerated during verification, -after this amount of mistaken attempts, it won't be possible to -verify the token anymore and it will be necessary to request a new one. - -``OPENWISP_RADIUS_SMS_TOKEN_MAX_USER_DAILY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``5`` - -The max number of SMS tokens a single user can request within a day. - -``OPENWISP_RADIUS_SMS_TOKEN_MAX_IP_DAILY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``999`` - -The max number of tokens which can be requested from the same IP address -during the same day. - -``OPENWISP_RADIUS_SMS_MESSAGE_TEMPLATE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``{organization} verification code: {code}`` - -The template used for sending verification code to users via SMS. - -.. note:: - - The template should always contain ``{code}`` placeholder. - Otherwise, the sent SMS will not contain the verification code. - -This value can be overridden per organization in the organization -change page. You can skip setting this option if you intend to set -it from organization change page for each organization. Keep in mind that -the default value is translated in other languages. If the value is -customized the translations will not work, so if you need this message -to be translated in different languages you should either not change the -default value or prepare the additional translations. - -OpenWISP Monitoring integration related settings -================================================ - -.. note:: - - This settings are only used if you have enabled the - :ref:`integration_with_openwisp_monitoring`. - -``OPENWISP_RADIUS_MONITORING_SHARED_ACCOUNTING`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Default**: ``False`` - -The monitoring integration performs a database lookup for the related -device using the ``called_station_id`` attribute from the RADIUS session. -If this is set to ``True``, the integration will disable the filtering of -devices by organization of the RADIUS session. - -This is useful when multiple organizations share the same captive portal -and the device's organization and RADIUS session organization are different. diff --git a/docs/user/radius_monitoring.rst b/docs/user/radius_monitoring.rst index 9838456e..8404490c 100644 --- a/docs/user/radius_monitoring.rst +++ b/docs/user/radius_monitoring.rst @@ -1,40 +1,40 @@ .. _integration_with_openwisp_monitoring: Integration with OpenWISP Monitoring ------------------------------------- +==================================== -OpenWISP RADIUS includes an optional Django sub-app that adds integration with -`OpenWISP Monitoring `_ -to provide RADIUS metrics. +OpenWISP RADIUS includes an optional Django sub-app that adds integration +with :doc:`OpenWISP Monitoring ` to provide RADIUS metrics. -.. image:: /images/radius-dashboard-charts.png - :alt: RADIUS session dashboard charts +.. image:: ../images/radius-dashboard-charts.png + :alt: RADIUS session dashboard charts RADIUS metrics -============== +-------------- 1. User registrations ~~~~~~~~~~~~~~~~~~~~~ -.. image:: /images/user-registration-chart.png - :alt: User registration chart +.. image:: ../images/user-registration-chart.png + :alt: User registration chart -This chart shows number of users signed up using different registration methods. +This chart shows number of users signed up using different registration +methods. 2. Total user registrations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: /images/total-user-registration-chart.png - :alt: Total user registration chart +.. image:: ../images/total-user-registration-chart.png + :alt: Total user registration chart -This chart shows total users registered using different registration methods -in the system on a given date. +This chart shows total users registered using different registration +methods in the system on a given date. 3. Unique RADIUS Sessions ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: /images/unique-radius-session-chart.png - :alt: Unique RADIUS session chart +.. image:: ../images/unique-radius-session-chart.png + :alt: Unique RADIUS session chart This chart shows unique RADIUS sessions. It is helpful to know how many unique users has used the system in a given time. @@ -42,37 +42,38 @@ unique users has used the system in a given time. 4. RADIUS traffic ~~~~~~~~~~~~~~~~~ -.. image:: /images/radius-traffic-chart.png - :alt: RADIUS traffic chart +.. image:: ../images/radius-traffic-chart.png + :alt: RADIUS traffic chart This chart shows the RADIUS traffic generated by user sessions. Enabling RADIUS metrics in Django project -========================================= +----------------------------------------- -You can enable the monitoring integration by including ``openwisp_radius.integrations.monitoring`` -in ``INSTALLED_APPS`` of your Django project's settings as following: +.. include:: /partials/settings-note.rst +You can enable the monitoring integration by including +``openwisp_radius.integrations.monitoring`` in ``INSTALLED_APPS`` of your +Django project's settings as following: .. code-block:: python # In your_project/settings.py - INSTALLED_APPS.append('openwisp_radius.integrations.monitoring') + INSTALLED_APPS.append("openwisp_radius.integrations.monitoring") .. note:: - Ensure your Django project is correctly configured to utilize OpenWISP Monitoring as - outlined in the `OpenWISP Monitoring's documentation `_. - For production environments, it is advisable to deploy OpenWISP using - `Ansible OpenWISP2 `_ or - `Docker OpenWISP `_, as they simplify - the deployment process considerably. + Ensure your Django project is correctly configured to utilize OpenWISP + Monitoring. For production environments, it is advisable to deploy OpenWISP using + :doc:`Ansible OpenWISP ` + or :doc:`Docker OpenWISP `, + as they simplify the deployment process considerably. .. important:: - If you are registering a :ref:`"registration method" ` - in any other Django application, then ``openwisp_radius.integrations.monitoring`` - should come after that app in the ``INSTALLED_APPS``. Otherwise, the + If you are registering a :ref:`"registration method" + ` in any other Django + application, then ``openwisp_radius.integrations.monitoring`` should + come after that app in the ``INSTALLED_APPS``. Otherwise, the registration method would not appear in the chart. - diff --git a/requirements-test.txt b/requirements-test.txt index 5c6b7355..9872eb49 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,7 +5,7 @@ packaging openwisp-sphinx-theme~=1.0.2 freezegun~=1.1.0 django-extensions -openwisp-utils[qa]~=1.1.0 +openwisp-utils[qa] @ https://github.com/openwisp/openwisp-utils/tarball/master pylinkvalidator lxml~=5.3.0 cssselect~=1.2.0 From dd9a682159d852e9ab866ea57e2a465d988b0a86 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 30 Oct 2024 21:58:14 +0530 Subject: [PATCH 50/51] [ci] Fixed docker command --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b5c069a..f06c6a4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: pip install ${{ matrix.django-version }} - name: Start InfluxDB and Redis container - run: docker-compose up -d influxdb redis + run: docker compose up -d influxdb redis - name: QA checks run: | From 36e7b0199b7ecc8c0581dadd70176ad35dede3c2 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 30 Oct 2024 22:03:04 +0530 Subject: [PATCH 51/51] [qa] Formatted docs --- .github/workflows/ci.yml | 6 +++--- docs/user/radius_monitoring.rst | 11 ++++++----- pyproject.toml | 1 - 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f06c6a4a..3098eea4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,11 +68,11 @@ jobs: - name: Tests if: ${{ !cancelled() && steps.deps.conclusion == 'success' }} run: | - coverage run runtests.py --parallel || ./runtests.py - MONITORING_INTEGRATION=1 coverage run --append runtests.py + coverage run --parallel-mode runtests.py --parallel || ./runtests.py # SAMPLE tests do not influence coverage, so we can speed up tests with --parallel - SAMPLE_APP=1 coverage run ./runtests.py --parallel > /dev/null 2>&1 || SAMPLE_APP=1 ./runtests.py + SAMPLE_APP=1 coverage run --parallel-mode ./runtests.py --parallel > /dev/null 2>&1 || SAMPLE_APP=1 ./runtests.py coverage combine + MONITORING_INTEGRATION=1 coverage run --append runtests.py coverage xml - name: Upload Coverage diff --git a/docs/user/radius_monitoring.rst b/docs/user/radius_monitoring.rst index 8404490c..7f80f0af 100644 --- a/docs/user/radius_monitoring.rst +++ b/docs/user/radius_monitoring.rst @@ -4,7 +4,8 @@ Integration with OpenWISP Monitoring ==================================== OpenWISP RADIUS includes an optional Django sub-app that adds integration -with :doc:`OpenWISP Monitoring ` to provide RADIUS metrics. +with :doc:`OpenWISP Monitoring ` to provide RADIUS +metrics. .. image:: ../images/radius-dashboard-charts.png :alt: RADIUS session dashboard charts @@ -65,10 +66,10 @@ Django project's settings as following: .. note:: Ensure your Django project is correctly configured to utilize OpenWISP - Monitoring. For production environments, it is advisable to deploy OpenWISP using - :doc:`Ansible OpenWISP ` - or :doc:`Docker OpenWISP `, - as they simplify the deployment process considerably. + Monitoring. For production environments, it is advisable to deploy + OpenWISP using :doc:`Ansible OpenWISP ` or + :doc:`Docker OpenWISP `, as they simplify the + deployment process considerably. .. important:: diff --git a/pyproject.toml b/pyproject.toml index 27522675..b6130200 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,5 @@ [tool.coverage.run] source = ["openwisp_radius"] -parallel = true concurrency = ["multiprocessing"] omit = [ "openwisp_radius/__init__.py",