Skip to content

Commit

Permalink
[req-change] Fixed mac roaming auth and added test
Browse files Browse the repository at this point in the history
  • Loading branch information
pandafy committed Oct 21, 2023
1 parent 8b5f05b commit 92cdcf2
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 48 deletions.
92 changes: 48 additions & 44 deletions openwisp_radius/api/freeradius_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django_filters.rest_framework import DjangoFilterBackend
from drf_yasg.utils import swagger_auto_schema
from ipware import get_client_ip
from rest_framework import status
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import (
AuthenticationFailed,
Expand All @@ -38,7 +39,9 @@
)
from .utils import IDVerificationHelper

RE_MAC_ADDR = re.compile(u'^{0}-{0}-{0}-{0}-{0}-{0}'.format(u'[a-f0-9]{2}'), re.I)
RE_MAC_ADDR = re.compile(
u'^{0}[:-]{0}[:-]{0}[:-]{0}[:-]{0}[:-]{0}'.format(u'[a-f0-9]{2}'), re.I
)
_TOKEN_AUTH_FAILED = _('Token authentication failed')
# Accounting-On and Accounting-Off are not implemented and
# hence ignored right now - may be implemented in the future
Expand Down Expand Up @@ -102,66 +105,49 @@ def _check_client_ip_and_return(self, request, uuid):
'settings.py is not a valid IP address. '
'Please contact administrator.'
).format(ip=ip)
logger.warning(invalid_addr_message)
raise AuthenticationFailed(invalid_addr_message)
message = _(
'Request rejected: Client IP address ({client_ip}) is not in '
'the list of IP addresses allowed to consume the freeradius API.'
).format(client_ip=client_ip)
logger.warning(message)
raise AuthenticationFailed(message)

def _mac_address_roaming_auth(self, username, request):
def _handle_mac_address_authentication(self, username, request):
if not username or not RE_MAC_ADDR.search(username):
# Username is either None or not a MAC addresss
return username, request
calling_station_id = RE_MAC_ADDR.match(username)[0]
logger.warn(f'calling station id -> {calling_station_id}')
# Get the most recent open session for the roaming user
open_session = (
RadiusAccounting.objects.select_related('organization__radius_settings')
.filter(calling_station_id=calling_station_id, stop_time=None)
.order_by('-start_time')
.first()
)
if open_session:
logger.warn(f'{open_session.username} - {open_session.called_station_id} - {open_session.calling_station_id}')
logger.warn(f'RadiusSetting - {open_session.organization.radius_settings.get_setting("mac_addr_roaming_enabled")}')
else:
logger.warn('No open session')
if open_session and open_session.organization.radius_settings.get_setting(
'mac_addr_roaming_enabled'
):
username = open_session.username
if hasattr(request.data, '_mutable'):
request.data._mutable = True
request.data['username'] = username
if hasattr(request.data, '_mutable'):
request.data._mutable = False
cache.set(
f'rt-{calling_station_id}',
str(open_session.organization_id),
86400,
if (
not open_session
or not open_session.organization.radius_settings.get_setting(
'mac_addr_roaming_enabled'
)
request._mac_allowed = True
return username, request
else:
return None, request

def _radius_token_authenticate(self, request):
if request.data.get('status_type', None) in UNSUPPORTED_STATUS_TYPES:
return

):
return None, None
username = open_session.username
if hasattr(request.data, '_mutable'):
request.data._mutable = True
request.data['username'] = username
if hasattr(request.data, '_mutable'):
request.data._mutable = False
request._mac_allowed = True
return username, request

def _radius_token_authenticate(self, username, request):
# cached_orgid exists only for users authenticated
# successfully in past 24 hours
username = request.data.get('username') or request.query_params.get('username')
cached_orgid = cache.get(f'rt-{username}')
if cached_orgid:
return self._check_client_ip_and_return(request, cached_orgid)
values = self._check_client_ip_and_return(request, cached_orgid)
return values
else:
logger.warn(f'from _radius_token_authenticate: {username} - {RE_MAC_ADDR.search(username)}')
if username and RE_MAC_ADDR.search(username):
username, request = self._mac_address_roaming_auth(username, request)
if username is None:
# raise AuthenticationFailed(_('Mac auth roaming failed'))
return
try:
radtoken = RadiusToken.objects.get(
user=auth_backend.get_users(username).first()
Expand All @@ -174,7 +160,6 @@ def _radius_token_authenticate(self, request):
)
else:
message = _('username field is required.')
logger.warning(message)
raise NotAuthenticated(message)
org_uuid = str(radtoken.organization_id)
cache.set(f'rt-{username}', org_uuid, 86400)
Expand All @@ -184,7 +169,24 @@ def authenticate(self, request):
self.check_organization(request)
uuid, token = self.get_uuid_token(request)
if not uuid and not token:
return self._radius_token_authenticate(request)
if request.data.get('status_type', None) in UNSUPPORTED_STATUS_TYPES:
return
username = request.data.get('username') or request.query_params.get(
'username'
)
username, request = self._handle_mac_address_authentication(
username, request
)
if username is None and request is None:
# When using MAC roaming, the "username" attribute contains the
# MAC address (calling_station_id). If there's no open-session
# for the given MAC address, then don't authenticate the user.
#
# NOTE: We return "None" here instead of raising "AuthenticationFailed"
# to prevent unnecessary authentication rejected errors in freeradius
# logs.
return
return self._radius_token_authenticate(username, request)
if not uuid or not token:
raise AuthenticationFailed(_TOKEN_AUTH_FAILED)
# check cache first
Expand Down Expand Up @@ -472,6 +474,8 @@ def post(self, request, *args, **kwargs):
does not return any JSON response so that freeradius will avoid
processing the response without generating warnings
"""
if request.user.is_anonymous and request.auth is None:
return Response(status=status.HTTP_200_OK)
data = request.data.copy()
if data.get('status_type', None) in UNSUPPORTED_STATUS_TYPES:
return Response(None)
Expand Down Expand Up @@ -548,13 +552,13 @@ def post(self, request, *args, **kwargs):
Returns an empty response body in order to instruct
FreeRADIUS to avoid processing the response body.
"""
if request.user.is_anonymous and request.auth is None:
return Response(status=status.HTTP_200_OK)
response = super().post(request, *args, **kwargs)
response.data = None
return response

def perform_create(self, serializer):
if self.request.auth is None:
return
organization = Organization.objects.get(pk=self.request.auth)
serializer.save(organization=organization)

Expand Down
102 changes: 98 additions & 4 deletions openwisp_radius/tests/test_api/test_freeradius_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1486,7 +1486,8 @@ def test_mac_addr_roaming_authorize_view(self):
username=acct_post_data['calling_station_id'],
password=acct_post_data['calling_station_id'],
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, None)

self._create_radius_accounting(**acct_post_data)

Expand All @@ -1495,7 +1496,8 @@ def test_mac_addr_roaming_authorize_view(self):
username=acct_post_data['calling_station_id'],
password=acct_post_data['calling_station_id'],
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, None)

OrganizationRadiusSettings.objects.update(mac_addr_roaming_enabled=True)
with self.subTest(
Expand Down Expand Up @@ -1540,7 +1542,8 @@ def test_mac_addr_roaming_accounting_view(self):
data=json.dumps(payload),
content_type='application/json',
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, None)

self._create_radius_accounting(update_time=now(), **acct_post_data)

Expand All @@ -1550,7 +1553,8 @@ def test_mac_addr_roaming_accounting_view(self):
data=json.dumps(payload),
content_type='application/json',
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, None)

OrganizationRadiusSettings.objects.update(mac_addr_roaming_enabled=True)
with self.subTest(
Expand Down Expand Up @@ -1589,6 +1593,96 @@ def test_mac_addr_roaming_accounting_view(self):
)
OrganizationRadiusSettings.objects.update(mac_addr_roaming_enabled=False)

def test_emulate_roaming(self):
"""
This tests emulates all the FreeRADIUS requests that are made
when user is roaming.
"""
self._get_org_user()
OrganizationRadiusSettings.objects.update(mac_addr_roaming_enabled=True)

rad_token = self._login_and_obtain_auth_token()
# Authorize to the first AP with username and radius token
response = self._authorize_user(password=rad_token)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data,
{
'control:Auth-Type': 'Accept',
'Session-Timeout': 10800,
'ChilliSpot-Max-Total-Octets': 3000000000,
},
)
# Start accounting session with first AP
payload = self._acct_initial_data.copy()
payload.update(
{
'username': 'tester',
'called_station_id': '11:22:33:44:55:66',
'calling_station_id': 'AA:BB:CC:DD:EE:FF',
'status_type': 'Start',
}
)
response = self.client.post(
self._acct_url,
data=json.dumps(payload),
content_type='application/json',
)
self.assertEqual(response.status_code, 201)
self.assertEqual(
RadiusAccounting.objects.filter(
username='tester',
stop_time=None,
called_station_id=payload['called_station_id'],
calling_station_id=payload['calling_station_id'],
).count(),
1,
)
# The automatic closing of session uses the value of the
# update_time field when closing old session.
RadiusAccounting.objects.update(update_time=now())

# User has moved to AP2 and the captive portal performs MAC auth
response = self._authorize_user(
username=payload['calling_station_id'],
password=payload['calling_station_id'],
)
self.assertEqual(response.status_code, 200)
# Start accounting session with the second AP
payload.update(
{
'session_id': '73',
'unique_id': '73',
'called_station_id': '11:22:33:44:55:67',
'username': payload['calling_station_id'],
}
)
response = self.client.post(
self._acct_url,
data=json.dumps(payload),
content_type='application/json',
)
self.assertEqual(RadiusAccounting.objects.filter(username='tester').count(), 2)
self.assertEqual(
RadiusAccounting.objects.filter(
username='tester',
stop_time=None,
called_station_id=payload['called_station_id'],
calling_station_id=payload['calling_station_id'],
).count(),
1,
)
# Accounting session with the first AP is closed
self.assertEqual(
RadiusAccounting.objects.filter(
username='tester',
stop_time__isnull=True,
called_station_id=payload['called_station_id'],
calling_station_id='AA:BB:CC:DD:EE:FF',
).count(),
1,
)


class TestApiReject(ApiTokenMixin, BaseTestCase):
@classmethod
Expand Down

0 comments on commit 92cdcf2

Please sign in to comment.