Skip to content

Commit

Permalink
Merge pull request openwisp#237 from Vivekrajput20/issue#232
Browse files Browse the repository at this point in the history
[authentication] Add RadiusToken openwisp#232
  • Loading branch information
nemesifier committed Jul 5, 2019
2 parents 979a091 + aaa203a commit 3674216
Show file tree
Hide file tree
Showing 23 changed files with 266 additions and 38 deletions.
11 changes: 9 additions & 2 deletions django_freeradius/admin.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import get_user_model

from . import settings as app_settings
from .base.admin import (
AbstractNasAdmin, AbstractRadiusAccountingAdmin, AbstractRadiusBatchAdmin, AbstractRadiusCheckAdmin,
AbstractRadiusGroupAdmin, AbstractRadiusGroupCheckAdmin, AbstractRadiusGroupReplyAdmin,
AbstractRadiusPostAuthAdmin, AbstractRadiusReplyAdmin, AbstractRadiusUserGroupAdmin, AbstractUserAdmin,
AbstractRadiusPostAuthAdmin, AbstractRadiusReplyAdmin, AbstractRadiusTokenAdmin,
AbstractRadiusUserGroupAdmin, AbstractUserAdmin,
)
from .models import (
Nas, RadiusAccounting, RadiusBatch, RadiusCheck, RadiusGroup, RadiusGroupCheck, RadiusGroupReply,
RadiusPostAuth, RadiusReply, RadiusUserGroup,
RadiusPostAuth, RadiusReply, RadiusToken, RadiusUserGroup,
)


Expand Down Expand Up @@ -66,6 +68,11 @@ class RadiusBatchAdmin(AbstractRadiusBatchAdmin):
pass


if settings.DEBUG:
@admin.register(RadiusToken)
class RadiusTokenAdmin(AbstractRadiusTokenAdmin):
pass

user_model = get_user_model()
admin.site.unregister(user_model)

Expand Down
34 changes: 21 additions & 13 deletions django_freeradius/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
RadiusAccounting = swapper.load_model('django_freeradius', 'RadiusAccounting')
User = get_user_model()
RadiusBatch = swapper.load_model('django_freeradius', 'RadiusBatch')
RadiusToken = swapper.load_model('django_freeradius', 'RadiusToken')

if app_settings.REST_USER_TOKEN_ENABLED:
from rest_framework.authtoken.models import Token
Expand Down Expand Up @@ -80,24 +81,20 @@ def authenticate_user(self, request, user):

def check_user_token(self, request, user):
"""
if user has no password set and has at least 1 social account
this is probably a social login, the password field is the
user's personal auth token
returns ``True`` if the password value supplied is a valid
radius user token
"""
if not app_settings.REST_USER_TOKEN_ENABLED:
return False

try:
token = Token.objects.get(
token = RadiusToken.objects.get(
user=user,
key=request.data.get('password')
)
except Token.DoesNotExist:
except RadiusToken.DoesNotExist:
token = None
else:
if app_settings.DISPOSABLE_USER_TOKEN:
token.delete()
finally:
return token is not None
if app_settings.DISPOSABLE_RADIUS_USER_TOKEN and token is not None:
token.delete()
return token is not None


authorize = AuthorizeView.as_view()
Expand Down Expand Up @@ -285,13 +282,21 @@ class ObtainAuthTokenView(BaseObtainAuthToken):
serializer_class = TokenSerializer
auth_serializer_class = AuthTokenSerializer
authentication_classes = []
radius_token = RadiusToken

def get_user(self, serializer):
"""
Designed to be overridden by extensions
"""
return serializer.validated_data['user']

def get_or_create_radius_token(self, user):
"""
Designed to be overridden by extensions
"""
radius_token, rad_token_created = self.radius_token.objects.get_or_create(user=user)
return radius_token

def post(self, request, *args, **kwargs):
serializer = self.auth_serializer_class(
data=request.data,
Expand All @@ -300,11 +305,14 @@ def post(self, request, *args, **kwargs):
serializer.is_valid(raise_exception=True)
user = self.get_user(serializer, *args, **kwargs)
token, created = Token.objects.get_or_create(user=user)
radius_token = self.get_or_create_radius_token(user)
context = {'view': self,
'request': request,
'token_login': True}
serializer = self.serializer_class(instance=token,
context=context)
return Response(serializer.data)
response = {'radius_user_token': radius_token.key}
response.update(serializer.data)
return Response(response)

obtain_auth_token = csrf_exempt(ObtainAuthTokenView.as_view())
6 changes: 6 additions & 0 deletions django_freeradius/base/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,9 @@ def get_inline_instances(self, request, obj=None):
self.admin_site)
inlines.append(usergroup)
return inlines


class AbstractRadiusTokenAdmin(ModelAdmin):
list_display = ['key', 'user', 'created']
fields = ['user']
ordering = ('-created',)
28 changes: 28 additions & 0 deletions django_freeradius/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,3 +819,31 @@ def _remove_files(self):
path = getattr(self, strategy_filemap.get(self.strategy)).path
if os.path.isfile(path):
os.remove(path)


class AbstractRadiusToken(TimeStampedEditableModel, models.Model):
# key field is a primary key so additional id field will be redundant
id = None
# tokens are not supposed to be modified, can be regenerated if necessary
modified = None
key = models.CharField(_('Key'), max_length=40, primary_key=True)
user = models.OneToOneField(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='radius_token')

class Meta:
db_table = 'radiustoken'
verbose_name = _('radius token')
verbose_name_plural = _('radius token')
abstract = True

def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()
return super().save(*args, **kwargs)

def generate_key(self):
return get_random_string(length=40)

def __str__(self):
return self.key
33 changes: 33 additions & 0 deletions django_freeradius/migrations/0004_radiustoken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 2.2 on 2019-06-07 00:37

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('django_freeradius', '0003_default_radius_groups'),
]

operations = [
migrations.CreateModel(
name='RadiusToken',
fields=[
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('key', models.CharField(max_length=40, primary_key=True, serialize=False, verbose_name='Key')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='radius_token', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'radius token',
'verbose_name_plural': 'radius token',
'db_table': 'radiustoken',
'abstract': False,
'swappable': 'DJANGO_FREERADIUS_RADIUSTOKEN_MODEL',
},
),
]
8 changes: 7 additions & 1 deletion django_freeradius/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .base.models import (
AbstractNas, AbstractRadiusAccounting, AbstractRadiusBatch, AbstractRadiusCheck, AbstractRadiusGroup,
AbstractRadiusGroupCheck, AbstractRadiusGroupReply, AbstractRadiusPostAuth, AbstractRadiusReply,
AbstractRadiusUserGroup,
AbstractRadiusToken, AbstractRadiusUserGroup,
)


Expand Down Expand Up @@ -65,3 +65,9 @@ class RadiusBatch(AbstractRadiusBatch):
class Meta(AbstractRadiusBatch.Meta):
abstract = False
swappable = swappable_setting('django_freeradius', 'RadiusBatch')


class RadiusToken(AbstractRadiusToken):
class Meta(AbstractRadiusToken.Meta):
abstract = False
swappable = swappable_setting('django_freeradius', 'RadiusToken')
4 changes: 2 additions & 2 deletions django_freeradius/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
API_TOKEN = getattr(settings, 'DJANGO_FREERADIUS_API_TOKEN', None)
API_AUTHORIZE_REJECT = getattr(settings, 'DJANGO_FREERADIUS_API_AUTHORIZE_REJECT', False)
REST_USER_TOKEN_ENABLED = 'rest_framework.authtoken' in settings.INSTALLED_APPS
SOCIAL_LOGIN_ENABLED = 'allauth.socialaccount' in settings.INSTALLED_APPS and REST_USER_TOKEN_ENABLED
DISPOSABLE_USER_TOKEN = getattr(settings, 'DJANGO_FREERADIUS_DISPOSABLE_USER_TOKEN', True)
SOCIAL_LOGIN_ENABLED = 'allauth.socialaccount' in settings.INSTALLED_APPS
DISPOSABLE_RADIUS_USER_TOKEN = getattr(settings, 'DJANGO_FREERADIUS_DISPOSABLE_RADIUS_USER_TOKEN', True)
API_ACCOUNTING_AUTO_GROUP = getattr(settings, 'DJANGO_FREERADIUS_API_ACCOUNTING_AUTO_GROUP', True)
EXTRA_NAS_TYPES = getattr(settings, 'DJANGO_FREERADIUS_EXTRA_NAS_TYPES', tuple())
10 changes: 9 additions & 1 deletion django_freeradius/social/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import swapper
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _
from django.views import View
from rest_framework.authtoken.models import Token

RadiusToken = swapper.load_model('django_freeradius', 'RadiusToken')


class RedirectCaptivePageView(View):
http_method_names = ['get']
Expand Down Expand Up @@ -35,8 +38,13 @@ def get_redirect_url(self, request):
cp = request.GET.get('cp')
user = request.user
Token.objects.filter(user=user).delete()
RadiusToken.objects.filter(user=user).delete()
token = Token.objects.create(user=user)
return '{0}?username={1}&token={2}'.format(cp, user.username, token.key)
rad_token = RadiusToken.objects.create(user=user)
return '{0}?username={1}&token={2}&radius_user_token={3}'.format(cp,
user.username,
token.key,
rad_token.key)


redirect_cp = RedirectCaptivePageView.as_view()
30 changes: 30 additions & 0 deletions django_freeradius/tests/base/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,33 @@ def test_batch_user_creation_form(self):
})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'errors field-number_of_users')

def test_radius_token_creation_form(self):
n = self.radius_token_model.objects.count()
url = reverse('admin:{0}_radiustoken_add'.format(self.app_name))
user = User.objects.create_user(username='test_user', password='test_password')
self.client.post(url, {'user': user.id})
self.assertEqual(self.radius_token_model.objects.count() - n, 1)

def test_radius_token_change(self):
user = User.objects.create_user(username='test_user', password='test_password')
token = self.radius_token_model.objects.create(user=user)
response = self.client.get(reverse(
'admin:{0}_radiustoken_change'.format(self.app_name),
args=[token.key]))
self.assertContains(response, 'ok')
self.assertNotContains(response, 'errors')

def test_radius_token_delete_selected(self):
user = User.objects.create_user(username='test_user', password='test_password')
token = self.radius_token_model.objects.create(user=user)
n = self.radius_token_model.objects.count()
url = reverse('admin:{0}_radiustoken_changelist'.format(self.app_name))
self.client.post(url, {
'action': 'delete_selected',
'_selected_action': str(token.key),
'select_across': '0',
'index': '0',
'post': 'yes'
}, follow=True)
self.assertEqual(n - self.radius_token_model.objects.count(), 1)
2 changes: 2 additions & 0 deletions django_freeradius/tests/base/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,8 @@ def test_user_auth_token_200(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['key'],
Token.objects.first().key)
self.assertEqual(response.data['radius_user_token'],
self.radius_token_model.objects.first().key)

def test_user_auth_token_400_credentials(self):
url = self._get_url()
Expand Down
14 changes: 14 additions & 0 deletions django_freeradius/tests/base/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,17 @@ def test_clean_method(self):
self.assertIn('Mixing', str(e))
else:
self.fail('ValidationError not raised')


class BaseTestRadiusToken(object):
def test_string_representation(self):
radiustoken = self.radius_token_model(key='test key')
self.assertEqual(str(radiustoken), radiustoken.key)

def test_create_radius_token_model(self):
u = get_user_model().objects.create(username='test',
email='test@test.org',
password='test')
obj = self.radius_token_model.objects.create(user=u)
self.assertEqual(str(obj), obj.key)
self.assertEqual(obj.user, u)
20 changes: 12 additions & 8 deletions django_freeradius/tests/base/test_social.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,30 @@ def test_redirect_cp_301(self):
r = self.client.get(url, {'cp': 'http://wifi.openwisp.org/cp'})
self.assertEqual(r.status_code, 302)
qs = Token.objects.filter(user=u)
rs = self.radius_token_model.objects.filter(user=u)
self.assertEqual(qs.count(), 1)
self.assertEqual(rs.count(), 1)
token = qs.first()
querystring = 'username={}&token={}'.format(u.username,
token.key)
rad_token = rs.first()
querystring = 'username={}&token={}&radius_user_token={}'.format(u.username,
token.key,
rad_token.key)
self.assertIn(querystring, r.url)

def test_authorize_using_user_token_200(self):
def test_authorize_using_radius_user_token_200(self):
self.test_redirect_cp_301()
token = Token.objects.filter(user__username='socialuser').first()
self.assertIsNotNone(token)
rad_token = self.radius_token_model.objects.filter(user__username='socialuser').first()
self.assertIsNotNone(rad_token)
response = self.client.post(reverse('freeradius:authorize'),
{'username': 'socialuser', 'password': token.key},
{'username': 'socialuser', 'password': rad_token.key},
HTTP_AUTHORIZATION=self.auth_header)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'control:Auth-Type': 'Accept'})

def test_authorize_using_user_token_403(self):
self.test_redirect_cp_301()
token = Token.objects.filter(user__username='socialuser').first()
self.assertIsNotNone(token)
rad_token = self.radius_token_model.objects.filter(user__username='socialuser').first()
self.assertIsNotNone(rad_token)
response = self.client.post(reverse('freeradius:authorize'),
{'username': 'socialuser', 'password': 'WRONG'},
HTTP_AUTHORIZATION=self.auth_header)
Expand Down
3 changes: 2 additions & 1 deletion django_freeradius/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from django_freeradius.models import (
Nas, RadiusAccounting, RadiusBatch, RadiusCheck, RadiusGroup, RadiusGroupCheck, RadiusGroupReply,
RadiusPostAuth, RadiusReply, RadiusUserGroup,
RadiusPostAuth, RadiusReply, RadiusToken, RadiusUserGroup,
)

from . import CallCommandMixin, CreateRadiusObjectsMixin, FileMixin, PostParamsMixin
Expand All @@ -26,3 +26,4 @@ class TestAdmin(FileMixin, CallCommandMixin, PostParamsMixin,
radius_reply_model = RadiusReply
radius_group_model = RadiusGroup
radius_usergroup_model = RadiusUserGroup
radius_token_model = RadiusToken
2 changes: 2 additions & 0 deletions django_freeradius/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
RadiusPostAuth = swapper.load_model('django_freeradius', 'RadiusPostAuth')
RadiusAccounting = swapper.load_model('django_freeradius', 'RadiusAccounting')
RadiusBatch = swapper.load_model('django_freeradius', 'RadiusBatch')
RadiusToken = swapper.load_model('django_freeradius', 'RadiusToken')


class ApiTestCase(PostParamsMixin, CreateRadiusObjectsMixin, TestCase):
radius_postauth_model = RadiusPostAuth
radius_accounting_model = RadiusAccounting
radius_batch_model = RadiusBatch
radius_token_model = RadiusToken
user_model = get_user_model()
radius_usergroup_model = RadiusUserGroup
auth_header = 'Bearer {}'.format(app_settings.API_TOKEN)
Expand Down
9 changes: 7 additions & 2 deletions django_freeradius/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

from django_freeradius.models import (
Nas, RadiusAccounting, RadiusBatch, RadiusCheck, RadiusGroup, RadiusGroupCheck, RadiusGroupReply,
RadiusPostAuth, RadiusReply, RadiusUserGroup,
RadiusPostAuth, RadiusReply, RadiusToken, RadiusUserGroup,
)

from . import CreateRadiusObjectsMixin
from .base.test_models import (
BaseTestNas, BaseTestRadiusAccounting, BaseTestRadiusBatch, BaseTestRadiusCheck, BaseTestRadiusGroup,
BaseTestRadiusPostAuth, BaseTestRadiusReply,
BaseTestRadiusPostAuth, BaseTestRadiusReply, BaseTestRadiusToken,
)


Expand Down Expand Up @@ -55,3 +55,8 @@ class TestRadiusPostAuth(BaseTestRadiusPostAuth, BaseTests):
@skipIf(os.environ.get('SAMPLE_APP', False), 'Running tests on SAMPLE_APP')
class TestRadiusBatch(BaseTestRadiusBatch, BaseTests):
radius_batch_model = RadiusBatch


@skipIf(os.environ.get('SAMPLE_APP', False), 'Running tests on SAMPLE_APP')
class TestRadiusToken(BaseTestRadiusToken, BaseTests):
radius_token_model = RadiusToken
Loading

0 comments on commit 3674216

Please sign in to comment.