320 lines
9.2 KiB
Python
320 lines
9.2 KiB
Python
# IP address part of this of this copied from
|
|
# https://github.com/benliles/django-ipauth/blob/master/ipauth/models.py
|
|
import logging
|
|
import re
|
|
from functools import total_ordering
|
|
|
|
from django.contrib.auth.models import Group
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.core import validators
|
|
from django.db import models
|
|
from django.db.models.signals import post_save
|
|
from django.db.utils import OperationalError
|
|
from django.forms import GenericIPAddressField as BaseIPAddressField
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class Library(models.Model):
|
|
'''
|
|
name and other things derive from the User
|
|
'''
|
|
user = models.OneToOneField(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='library',
|
|
)
|
|
group = models.OneToOneField(
|
|
Group,
|
|
on_delete=models.CASCADE,
|
|
related_name='library',
|
|
null=True
|
|
)
|
|
backend = models.CharField(max_length=10, choices=(
|
|
('ip', 'IP authentication'),
|
|
('cardnum', 'Library Card Number check'),
|
|
('email', 'e-mail pattern check'),
|
|
), default='ip')
|
|
name = models.CharField(max_length=80, default='')
|
|
approved = models.BooleanField(default=False)
|
|
owner = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name="libraries",
|
|
)
|
|
credential = None
|
|
|
|
def __str__(self):
|
|
return str(self.name)
|
|
|
|
def add_user(self, user):
|
|
user.groups.add(self.group)
|
|
(library_user, created) = LibraryUser.objects.get_or_create(library=self, user=user)
|
|
library_user.credential = self.credential
|
|
library_user.save()
|
|
|
|
def has_user(self, user):
|
|
return self.group in user.groups.all() or user == self.user
|
|
|
|
@property
|
|
def join_template(self):
|
|
if self.approved:
|
|
return 'libraryauth/' + self.backend + '_join.html'
|
|
return 'libraryauth/unapproved.html'
|
|
|
|
@property
|
|
def help_template(self):
|
|
return 'libraryauth/' + self.backend + '_help.html'
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('library', args=[self.user.username])
|
|
|
|
def add_group(sender, created, instance, **kwargs):
|
|
if created:
|
|
num = ''
|
|
while created:
|
|
(group, created) = Group.objects.get_or_create(name=instance.user.username + num)
|
|
# make sure not using a group twice!
|
|
if created:
|
|
created = False
|
|
else:
|
|
num += '+'
|
|
try:
|
|
group.library
|
|
created = True
|
|
except Library.DoesNotExist:
|
|
pass
|
|
instance.group = group
|
|
instance.save()
|
|
|
|
post_save.connect(add_group, sender=Library)
|
|
|
|
def ip_to_int(value):
|
|
validators.validate_ipv4_address(value)
|
|
|
|
lower_validator = validators.MinValueValidator(0)
|
|
upper_validator = validators.MaxValueValidator(255)
|
|
|
|
value = [int(octet) for octet in value.split('.')]
|
|
output = 0
|
|
|
|
for i in range(0, 4):
|
|
validators.validate_integer(value[i])
|
|
lower_validator(value[i])
|
|
upper_validator(value[i])
|
|
output += value[i] * (256**(3-i))
|
|
|
|
return output
|
|
|
|
def int_to_ip(value):
|
|
validators.validate_integer(value)
|
|
value = int(value)
|
|
|
|
validators.MinValueValidator(0)(value)
|
|
validators.MaxValueValidator(4294967295)(value)
|
|
|
|
return '%d.%d.%d.%d' % (value >> 24, value >> 16 & 255,
|
|
value >> 8 & 255, value & 255)
|
|
|
|
@total_ordering
|
|
class IP(object):
|
|
def __init__(self, value):
|
|
self.int = value
|
|
|
|
def _set_int(self, value):
|
|
if isinstance(value, IP):
|
|
self._int = IP.int
|
|
|
|
try:
|
|
self._int = int(value)
|
|
except ValueError:
|
|
self._int = ip_to_int(value)
|
|
except (TypeError, ValidationError):
|
|
self._int = None
|
|
|
|
def _get_int(self):
|
|
return self._int
|
|
|
|
int = property(_get_int, _set_int)
|
|
|
|
def _get_str(self):
|
|
if self.int != None:
|
|
return int_to_ip(self.int)
|
|
return ''
|
|
|
|
string = property(_get_str, _set_int)
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, IP):
|
|
try:
|
|
other = IP(other)
|
|
except:
|
|
return False
|
|
|
|
return self.int == other.int
|
|
|
|
def __gt__(self, other):
|
|
if not isinstance(other, IP):
|
|
other = IP(other)
|
|
|
|
if self.int is not None and other.int is not None:
|
|
return self.int.__gt__(other.int)
|
|
|
|
raise ValueError('Invalid arguments')
|
|
|
|
def __str__(self):
|
|
return self.string
|
|
|
|
class IPAddressFormField(BaseIPAddressField):
|
|
default_validators = []
|
|
|
|
def prepare_value(self, value):
|
|
if isinstance(value, IP):
|
|
return value.string
|
|
|
|
try:
|
|
return IP(value).string
|
|
except:
|
|
pass
|
|
|
|
return value
|
|
|
|
def to_python(self, value):
|
|
if value == 0:
|
|
return IP(0)
|
|
if value in validators.EMPTY_VALUES:
|
|
return None
|
|
|
|
try:
|
|
return IP(value)
|
|
except ValidationError:
|
|
raise ValidationError(self.default_error_messages['invalid'],
|
|
code='invalid')
|
|
|
|
class IPAddressModelField(models.GenericIPAddressField):
|
|
empty_strings_allowed = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
models.Field.__init__(self, *args, **kwargs)
|
|
|
|
def get_internal_type(self):
|
|
return "PositiveIntegerField"
|
|
|
|
def get_prep_value(self, value):
|
|
if not value:
|
|
return value
|
|
|
|
if isinstance(value, IP):
|
|
return value.int
|
|
|
|
def to_python(self, value):
|
|
if isinstance(value, IP):
|
|
return value
|
|
try:
|
|
return IP(value)
|
|
except ValidationError:
|
|
return None
|
|
|
|
def formfield(self, **kwargs):
|
|
defaults = {'form_class': IPAddressFormField}
|
|
defaults.update(kwargs)
|
|
return super(models.GenericIPAddressField, self).formfield(**defaults)
|
|
|
|
def deconstruct(self):
|
|
name, path, args, kwargs = super(models.GenericIPAddressField, self).deconstruct()
|
|
return name, path, args, kwargs
|
|
|
|
|
|
class Block(models.Model):
|
|
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name='ip_auths')
|
|
lower = IPAddressModelField(db_index=True, unique=True)
|
|
upper = IPAddressModelField(db_index=True, blank=True, null=True)
|
|
|
|
def clean(self):
|
|
if self.upper and self.upper.int:
|
|
try:
|
|
if self.lower > self.upper:
|
|
raise ValidationError('Lower end of the Block must be less '
|
|
'than or equal to the upper end')
|
|
except ValueError as e:
|
|
pass
|
|
|
|
|
|
def __str__(self):
|
|
if self.upper and self.upper.int:
|
|
return u'%s %s-%s' % (self.library, self.lower, self.upper)
|
|
return u'%s %s' % (self.library, self.lower)
|
|
|
|
class Meta:
|
|
ordering = ['lower',]
|
|
|
|
|
|
# from https://en.wikipedia.org/wiki/Luhn_algorithm#Implementation_of_standard_Mod_10
|
|
def luhn_checksum(card_number):
|
|
def digits_of(n):
|
|
return [int(d) for d in str(n)]
|
|
digits = digits_of(card_number)
|
|
odd_digits = digits[-1::-2]
|
|
even_digits = digits[-2::-2]
|
|
checksum = 0
|
|
checksum += sum(odd_digits)
|
|
for d in even_digits:
|
|
checksum += sum(digits_of(d*2))
|
|
return checksum % 10
|
|
|
|
class CardPattern(models.Model):
|
|
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name='cardnum_auths')
|
|
# match pattern ^\d+#+$
|
|
pattern = models.CharField(max_length=20)
|
|
checksum = models.BooleanField(default=True)
|
|
|
|
def is_valid(self, card_number):
|
|
match_pattern = '^' + self.pattern.replace('#', r'\d', 20) + '$'
|
|
if re.match(match_pattern, card_number) is None:
|
|
return False
|
|
if self.checksum:
|
|
return luhn_checksum(card_number) == 0
|
|
return True
|
|
|
|
class LibraryUser(models.Model):
|
|
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name='library_users')
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='user_libraries',
|
|
)
|
|
credential = models.CharField(max_length=30, null=True)
|
|
date_modified = models.DateTimeField(auto_now=True)
|
|
|
|
class EmailPattern(models.Model):
|
|
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name='email_auths')
|
|
# email endswith string
|
|
pattern = models.CharField(max_length=20)
|
|
|
|
def is_valid(self, email):
|
|
return email.lower().endswith(self.pattern.lower())
|
|
|
|
class BadUsernamePattern(models.Model):
|
|
pattern = models.CharField(max_length=100)
|
|
last = models.DateTimeField(default=timezone.now)
|
|
|
|
def matches(self, username):
|
|
if re.match(self.pattern, username):
|
|
self.last = timezone.now()
|
|
self.save()
|
|
return True
|
|
return False
|
|
|
|
def get_special():
|
|
try:
|
|
specials = Library.objects.filter(user__username='special')
|
|
for special in specials:
|
|
return special
|
|
return None
|
|
except OperationalError:
|
|
# database not loaded yet, for example during testing
|
|
return None
|
|
|