Skip to content
This repository has been archived by the owner on Sep 10, 2020. It is now read-only.

Commit

Permalink
[RadiusCheck] Password encryption in RadiusCheck #58
Browse files Browse the repository at this point in the history
- RadiusCheckAdmin password value auto hash by enabled password type (default NT-Password)
- RadiusCheckAdmin custom filters: find duplicates by username or value, find is_active true or false and find valid_until < or > then now()
- RadiusCheck custom model manager
- _encode_secret standalone funcion in base/models.py, still needs a better position! It should not be forget into models.py but moved in an apposite file. Should it be called helpers.py?
- enhanced app_settings.py approach, nemesys revision
- freeradius documentation, how to do to extend radiucheck query. A mysql dialect example

Fixes #58
Improvements for #35 and #63
  • Loading branch information
peppelinux committed Dec 19, 2017
1 parent 25dc8d1 commit abfe593
Show file tree
Hide file tree
Showing 15 changed files with 473 additions and 62 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.rst
@@ -1,6 +1,6 @@
Contributing
============

Thanks for your interest! Pllease read `our contributing guideliness
Thanks for your interest! Please read `our contributing guideliness
<http://django-freeradius.readthedocs.io/en/latest/general/contributing.html>`_
and submit a PR.
25 changes: 21 additions & 4 deletions django_freeradius/base/admin.py
@@ -1,6 +1,11 @@
from django.contrib.admin import ModelAdmin
from django.contrib.admin.actions import delete_selected

from .. import settings as app_settings
from .admin_actions import disable_accounts, enable_accounts
from .admin_filters import DuplicateListFilter, ExpiredListFilter
from .forms import AbstractRadiusCheckAdminForm
from .models import _encode_secret


class TimeStampedEditableAdmin(ModelAdmin):
Expand Down Expand Up @@ -59,10 +64,22 @@ class AbstractRadiusGroupUsersAdmin(TimeStampedEditableAdmin):


class AbstractRadiusCheckAdmin(TimeStampedEditableAdmin):
list_display = ('username', 'attribute', 'value', 'is_active',
'created', 'modified')
search_fields = ('username',)
list_filter = ('created', 'modified')
list_display = ('username', 'attribute', 'is_active',
'created', 'valid_until')
search_fields = ('username', 'value')
list_filter = (DuplicateListFilter, ExpiredListFilter, 'created',
'modified', 'valid_until')
readonly_fields = ('value',)
form = AbstractRadiusCheckAdminForm
fields = ('username', 'value', 'op', 'attribute', 'new_value',
'is_active', 'valid_until', 'note', 'created', 'modified')
actions = [disable_accounts, enable_accounts, delete_selected]

def save_model(self, request, obj, form, change):
if form.data.get('new_value'):
obj.value = _encode_secret(form.data['attribute'],
form.data.get('new_value'))
obj.save()


class AbstractRadiusReplyAdmin(TimeStampedEditableAdmin):
Expand Down
36 changes: 36 additions & 0 deletions django_freeradius/base/admin_actions.py
@@ -0,0 +1,36 @@
from django.contrib import messages
from django.contrib.admin.models import CHANGE, LogEntry
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _


def disable_accounts(modeladmin, request, queryset):
queryset.update(is_active=False)
ct = ContentType.objects.get_for_model(queryset.model)
for entry in queryset:
LogEntry.objects.log_action(user_id=request.user.id,
content_type_id=ct.pk,
object_id=entry.pk,
object_repr=entry.username,
action_flag=CHANGE,
change_message=_("Disabled"))
messages.add_message(request, messages.INFO, '%d modifiche' % queryset.count())


disable_accounts.short_description = _('Disable')


def enable_accounts(modeladmin, request, queryset):
queryset.update(is_active=True)
ct = ContentType.objects.get_for_model(queryset.model)
for entry in queryset:
LogEntry.objects.log_action(user_id=request.user.id,
content_type_id=ct.pk,
object_id=entry.pk,
object_repr=entry.username,
action_flag=CHANGE,
change_message=_("Enabled"))
messages.add_message(request, messages.INFO, '%d modifiche' % queryset.count())


enable_accounts.short_description = _('Enable')
30 changes: 30 additions & 0 deletions django_freeradius/base/admin_filters.py
@@ -0,0 +1,30 @@
from django.contrib.admin import SimpleListFilter
from django.utils.translation import ugettext_lazy as _


class DuplicateListFilter(SimpleListFilter):
title = _('find duplicates')
parameter_name = 'duplicates'

def lookups(self, request, model_admin):
return (('username', _('username')), ('value', _('value')))

def queryset(self, request, queryset):
if self.value() == 'value':
return queryset.filter_duplicate_value()
elif self.value() == 'username':
return queryset.filter_duplicate_username()


class ExpiredListFilter(SimpleListFilter):
title = _('find expired')
parameter_name = 'expired'

def lookups(self, request, model_admin):
return (('expired', _('expired')), ('not_expired', _('not expired')))

def queryset(self, request, queryset):
if self.value() == 'expired':
return queryset.filter_expired()
elif self.value() == 'not_expired':
return queryset.filter_not_expired()
37 changes: 37 additions & 0 deletions django_freeradius/base/forms.py
@@ -0,0 +1,37 @@
import re

from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

from .. import settings as app_settings
from .models import AbstractRadiusCheck


class AbstractRadiusCheckAdminForm(forms.ModelForm):
_secret_help_text = _('The secret must contains lowercase'
' and uppercase characters, '
' number and at least one of these symbols:'
'! % - _ + = [ ] { } : , . ? < > ( ) ; ')
# custom field not backed by database
new_value = forms.CharField(label=_('Value'), required=False,
min_length=8, max_length=16,
widget=forms.PasswordInput(),
help_text=_secret_help_text)

def clean_attribute(self):
if self.data['attribute'] not in app_settings.DISABLED_SECRET_FORMATS:
return self.cleaned_data["attribute"]

def clean_new_value(self):
if not self.data['new_value']:
return None
for regexp in app_settings.RADCHECK_SECRET_VALIDATORS.values():
found = re.findall(regexp, self.data['new_value'])
if not found:
raise ValidationError(self._secret_help_text)
return self.cleaned_data["new_value"]

class Meta:
model = AbstractRadiusCheck
fields = '__all__'
114 changes: 81 additions & 33 deletions django_freeradius/base/models.py
@@ -1,40 +1,38 @@
from django.db import models
from django.db.models import Count
from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from model_utils.fields import AutoCreatedField, AutoLastModifiedField

RADOP_CHECK_TYPES = (
('=', '='),
(':=', ':='),
('==', '=='),
('+=', '+='),
('!=', '!='),
('>', '>'),
('>=', '>='),
('<', '<'),
('<=', '<='),
('=~', '=~'),
('!~', '!~'),
('=*', '=*'),
('!*', '!*'),
)

RADOP_REPLY_TYPES = (
('=', '='),
(':=', ':='),
('+=', '+='),
)

RADCHECK_PASSWD_TYPE = (
('Cleartext-Password', 'Cleartext-Password'),
('NT-Password', 'NT-Password'),
('LM-Password', 'LM-Password'),
('MD5-Password', 'MD5-Password'),
('SMD5-Password', 'SMD5-Password'),
('SSHA-Password', 'SSHA-Password'),
('Crypt-Password', 'Crypt-Password'),
)
from passlib.hash import lmhash, nthash

from .. import settings as app_settings

RADOP_CHECK_TYPES = (('=', '='),
(':=', ':='),
('==', '=='),
('+=', '+='),
('!=', '!='),
('>', '>'),
('>=', '>='),
('<', '<'),
('<=', '<='),
('=~', '=~'),
('!~', '!~'),
('=*', '=*'),
('!*', '!*'))

RADOP_REPLY_TYPES = (('=', '='),
(':=', ':='),
('+=', '+='))

RADCHECK_PASSWD_TYPE = ['Cleartext-Password',
'NT-Password',
'LM-Password',
'MD5-Password',
'SMD5-Password',
'SSHA-Password',
'Crypt-Password']


class TimeStampedEditableModel(models.Model):
Expand Down Expand Up @@ -123,6 +121,48 @@ def __str__(self):
return self.username


class AbstractRadiusCheckQueryset(models.query.QuerySet):
def filter_duplicate_username(self):
pks = []
for i in self.values('username').annotate(Count('id')).order_by().filter(id__count__gt=1):
pks.extend([account.pk for account in self.filter(username=i['username'])])
return self.filter(pk__in=pks)

def filter_duplicate_value(self):
pks = []
for i in self.values('value').annotate(Count('id')).order_by().filter(id__count__gt=1):
pks.extend([accounts.pk for accounts in self.filter(value=i['value'])])
return self.filter(pk__in=pks)

def filter_expired(self):
return self.filter(valid_until__lt=now())

def filter_not_expired(self):
return self.filter(valid_until__gte=now())


def _encode_secret(attribute, new_value=None):
if attribute == 'Cleartext-Password':
password_renewed = new_value
elif attribute == 'NT-Password':
password_renewed = nthash.hash(new_value)
elif attribute == 'LM-Password':
password_renewed = lmhash.hash(new_value)
return password_renewed


class AbstractRadiusCheckManager(models.Manager):
def get_queryset(self):
return AbstractRadiusCheckQueryset(self.model, using=self._db)

def create(self, *args, **kwargs):
if 'new_value' in kwargs:
kwargs['value'] = _encode_secret(kwargs['attribute'],
kwargs['new_value'])
del(kwargs['new_value'])
return super(AbstractRadiusCheckManager, self).create(*args, **kwargs)


@python_2_unicode_compatible
class AbstractRadiusCheck(TimeStampedEditableModel):
username = models.CharField(verbose_name=_('username'),
Expand All @@ -134,8 +174,16 @@ class AbstractRadiusCheck(TimeStampedEditableModel):
choices=RADOP_CHECK_TYPES,
default=':=')
attribute = models.CharField(verbose_name=_('attribute'),
max_length=64, choices=RADCHECK_PASSWD_TYPE)
max_length=64,
choices=[(i, i) for i in RADCHECK_PASSWD_TYPE
if i not in
app_settings.DISABLED_SECRET_FORMATS],
blank=True,
default=app_settings.DEFAULT_SECRET_FORMAT)
is_active = models.BooleanField(default=True)
valid_until = models.DateTimeField(null=True, blank=True)
note = models.TextField(null=True, blank=True)
objects = AbstractRadiusCheckManager()

class Meta:
db_table = 'radcheck'
Expand Down
20 changes: 20 additions & 0 deletions django_freeradius/migrations/0009_radiuscheck_expires.py
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-03 11:18
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('django_freeradius', '0008_auto_20171004_1003'),
]

operations = [
migrations.AddField(
model_name='radiuscheck',
name='expires',
field=models.DateTimeField(blank=True, null=True),
),
]
20 changes: 20 additions & 0 deletions django_freeradius/migrations/0010_auto_20171107_1158.py
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-07 10:58
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('django_freeradius', '0009_radiuscheck_expires'),
]

operations = [
migrations.RenameField(
model_name='radiuscheck',
old_name='expires',
new_name='valid_until',
),
]
20 changes: 20 additions & 0 deletions django_freeradius/migrations/0011_radiuscheck_note.py
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-08 14:46
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('django_freeradius', '0010_auto_20171107_1158'),
]

operations = [
migrations.AddField(
model_name='radiuscheck',
name='note',
field=models.TextField(blank=True, null=True),
),
]
25 changes: 25 additions & 0 deletions django_freeradius/migrations/0012_auto_20171206_1546.py
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2017-12-06 14:46
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('django_freeradius', '0011_radiuscheck_note'),
]

operations = [
migrations.AlterField(
model_name='radiuscheck',
name='attribute',
field=models.CharField(blank=True, choices=[('Cleartext-Password', 'Cleartext-Password'), ('NT-Password', 'NT-Password'), ('LM-Password', 'LM-Password'), ('MD5-Password', 'MD5-Password'), ('SMD5-Password', 'SMD5-Password'), ('SSHA-Password', 'SSHA-Password'), ('Crypt-Password', 'Crypt-Password')], default='NT-Password', max_length=64, verbose_name='attribute'),
),
migrations.AlterField(
model_name='radiuscheck',
name='is_active',
field=models.BooleanField(default=True),
),
]

0 comments on commit abfe593

Please sign in to comment.