team-3/src/c3nav/mapdata/models/access.py

266 lines
10 KiB
Python
Raw Normal View History

2017-12-10 03:16:07 +01:00
import pickle
import uuid
2017-12-10 14:13:20 +01:00
from collections import namedtuple
from datetime import timedelta
2017-12-10 14:46:41 +01:00
from typing import Sequence
2017-10-24 22:13:53 +02:00
from django.conf import settings
2017-10-24 22:45:57 +02:00
from django.core.cache import cache
from django.db import models, transaction
from django.db.models import Q
2017-10-24 22:45:57 +02:00
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
2017-12-10 14:46:41 +01:00
from django.utils.translation import ungettext_lazy
2017-10-27 16:40:15 +02:00
from c3nav.mapdata.models import MapUpdate
from c3nav.mapdata.models.base import SerializableMixin, TitledMixin
class AccessRestriction(TitledMixin, models.Model):
"""
2017-10-24 22:13:53 +02:00
An access restriction
"""
2017-10-24 22:13:53 +02:00
open = models.BooleanField(default=False, verbose_name=_('open'))
2017-12-20 20:54:58 +01:00
groups = models.ManyToManyField('mapdata.AccessRestrictionGroup', verbose_name=_('Groups'), blank=True)
class Meta:
verbose_name = _('Access Restriction')
verbose_name_plural = _('Access Restrictions')
default_related_name = 'accessrestrictions'
def _serialize(self, **kwargs):
result = super()._serialize(**kwargs)
result['groups'] = tuple(group.pk for group in self.groups.all())
return result
@classmethod
def qs_for_request(cls, request):
return cls.objects.filter(cls.q_for_request(request))
@classmethod
def q_for_request(cls, request):
return Q(pk__in=AccessPermission.get_for_request(request))
2017-12-20 20:54:58 +01:00
class AccessRestrictionGroup(TitledMixin, models.Model):
"""
An access restriction group
"""
class Meta:
verbose_name = _('Access Restriction Group')
verbose_name_plural = _('Access Restriction Groups')
default_related_name = 'accessrestrictiongroups'
@classmethod
def qs_for_request(cls, request):
return cls.objects.filter(cls.q_for_request(request))
@classmethod
def q_for_request(cls, request):
if request.user.is_authenticated and request.user.is_superuser:
return Q()
permissions = AccessPermission.get_for_request(request)
return Q(Q(accessrestrictions=None) | Q(accessrestrictions__pk__in=permissions))
2017-12-10 03:16:07 +01:00
def default_valid_until():
return timezone.now()+timedelta(seconds=20)
2017-12-10 14:13:20 +01:00
AccessPermissionTokenItem = namedtuple('AccessPermissionTokenItem', ('pk', 'expire_date', 'title'))
2017-12-10 03:16:07 +01:00
class AccessPermissionToken(models.Model):
2017-12-18 14:54:45 +01:00
token = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
2017-12-10 03:16:07 +01:00
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
related_name='created_accesspermission_tokens',
verbose_name=_('author'))
valid_until = models.DateTimeField(db_index=True, default=default_valid_until,
verbose_name=_('valid until'))
unlimited = models.BooleanField(default=False, db_index=True, verbose_name=_('unlimited'))
redeemed = models.BooleanField(default=False, db_index=True, verbose_name=_('redeemed'))
can_grant = models.BooleanField(default=False, db_index=True, verbose_name=_('can grant'))
2017-12-19 12:19:31 +01:00
unique_key = models.CharField(max_length=32, null=True, verbose_name=_('unique key'))
2017-12-10 03:16:07 +01:00
data = models.BinaryField()
2017-12-19 12:19:31 +01:00
class Meta:
verbose_name = _('Access Permission Token')
verbose_name_plural = _('Access Permission Tokens')
default_related_name = 'accessrestriction_tokens'
2017-12-10 03:16:07 +01:00
@property
2017-12-10 14:46:41 +01:00
def restrictions(self) -> Sequence[AccessPermissionTokenItem]:
2017-12-10 03:16:07 +01:00
return pickle.loads(self.data)
@restrictions.setter
2017-12-10 14:46:41 +01:00
def restrictions(self, value: Sequence[AccessPermissionTokenItem]):
2017-12-10 03:16:07 +01:00
self.data = pickle.dumps(value)
2017-12-10 14:46:41 +01:00
class RedeemError(Exception):
pass
2017-12-10 03:16:07 +01:00
def redeem(self, user=None):
if (user is None and self.redeemed) or self.accesspermissions.exists():
2017-12-10 14:46:41 +01:00
raise self.RedeemError('Already redeemed.')
2017-12-10 14:46:41 +01:00
if timezone.now() > self.valid_until + timedelta(minutes=5 if self.redeemed else 0):
raise self.RedeemError('No longer valid.')
2017-12-10 03:16:07 +01:00
if user:
2017-12-19 12:19:31 +01:00
with transaction.atomic():
if self.author_id and self.unique_key:
AccessPermission.objects.filter(author_id=self.author_id, unique_key=self.unique_key).delete()
for restriction in self.restrictions:
AccessPermission.objects.create(
user=user,
access_restriction_id=restriction.pk,
author_id=self.author_id,
expire_date=restriction.expire_date,
can_grant=self.can_grant,
unique_key=self.unique_key,
token=self if self.pk else None,
)
2017-12-10 03:16:07 +01:00
if self.pk and not self.unlimited:
self.redeemed = True
2017-12-10 03:16:07 +01:00
self.save()
2017-12-10 03:49:21 +01:00
def bump(self):
self.valid_until = max(self.valid_until, default_valid_until())
2017-12-10 03:49:21 +01:00
2017-12-10 14:46:41 +01:00
@property
def redeem_success_message(self):
return ungettext_lazy('Area successfully unlocked.', 'Areas successfully unlocked.', len(self.restrictions))
2017-12-10 03:16:07 +01:00
2017-10-24 22:13:53 +02:00
class AccessPermission(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
access_restriction = models.ForeignKey(AccessRestriction, on_delete=models.CASCADE)
expire_date = models.DateTimeField(null=True, verbose_name=_('expires'))
2017-12-08 21:31:53 +01:00
can_grant = models.BooleanField(default=False, verbose_name=_('can grant'))
author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL,
related_name='authored_access_permissions', verbose_name=_('Author'))
2017-12-19 12:19:31 +01:00
unique_key = models.CharField(max_length=32, null=True, verbose_name=_('unique key'))
token = models.ForeignKey(AccessPermissionToken, null=True, on_delete=models.CASCADE,
verbose_name=_('Access permission token'))
2017-10-24 22:13:53 +02:00
class Meta:
verbose_name = _('Access Permission')
verbose_name_plural = _('Access Permissions')
default_related_name = 'accesspermissions'
2017-12-19 12:19:31 +01:00
unique_together = (
('author', 'unique_key')
)
2017-10-24 22:13:53 +02:00
2017-10-24 22:45:57 +02:00
@staticmethod
def user_access_permission_key(user_id):
return 'mapdata:user_access_permission:%d' % user_id
2017-10-24 22:45:57 +02:00
@classmethod
def queryset_for_user(cls, user, can_grant=None):
return user.accesspermissions.filter(
Q(expire_date__isnull=True) | Q(expire_date__gt=timezone.now())
).filter(
Q(can_grant=True) if can_grant is not None else Q()
)
2017-12-20 21:17:35 +01:00
@classmethod
def get_for_request_with_expire_date(cls, request, can_grant=None):
if not request.user.is_authenticated:
return {}
if request.user_permissions.grant_all_access:
return {pk: None for pk in cls.get_all_access_restrictions()}
result = tuple(
cls.queryset_for_user(request.user, can_grant).values_list('access_restriction_id', 'expire_date')
)
2017-12-20 21:17:35 +01:00
# collect permissions (can be multiple for one restriction)
permissions = {}
for access_restriction_id, expire_date in result:
permissions.setdefault(access_restriction_id, set()).add(expire_date)
# get latest expire date for each permission
permissions = {
access_restriction_id: None if None in expire_dates else max(expire_dates)
for access_restriction_id, expire_dates in permissions.items()
}
return permissions
@staticmethod
def get_all_access_restrictions():
cache_key = 'all_access_restrictions:%s' % MapUpdate.current_cache_key()
access_restriction_ids = cache.get(cache_key, None)
if access_restriction_ids is None:
access_restriction_ids = set(AccessRestriction.objects.values_list('pk', flat=True))
cache.set(cache_key, access_restriction_ids, 300)
return access_restriction_ids
2017-10-24 22:45:57 +02:00
@classmethod
def get_for_request(cls, request):
if not request.user.is_authenticated:
return set()
if request.user_permissions.grant_all_access:
2017-12-20 21:17:35 +01:00
return cls.get_all_access_restrictions()
cache_key = cls.user_access_permission_key(request.user.pk)
2017-10-24 22:45:57 +02:00
access_restriction_ids = cache.get(cache_key, None)
if access_restriction_ids is None:
2017-12-20 21:17:35 +01:00
permissions = cls.get_for_request_with_expire_date(request)
access_restriction_ids = set(permissions.keys())
expire_date = min((e for e in permissions.values() if e), default=timezone.now()+timedelta(seconds=120))
cache.set(cache_key, access_restriction_ids, max(0, (expire_date-timezone.now()).total_seconds()))
2017-10-24 23:24:45 +02:00
return set(access_restriction_ids)
2017-10-24 22:45:57 +02:00
2017-10-27 16:40:15 +02:00
@classmethod
2017-10-27 17:08:36 +02:00
def cache_key_for_request(cls, request, with_update=True):
return (
((MapUpdate.current_cache_key()+':') if with_update else '') +
2017-12-21 05:40:35 +01:00
'-'.join(str(i) for i in sorted(AccessPermission.get_for_request(request)) or '0')
2017-10-27 16:40:15 +02:00
)
@classmethod
def etag_func(cls, request, *args, **kwargs):
return cls.cache_key_for_request(request)
def save(self, *args, **kwargs):
with transaction.atomic():
super().save(*args, **kwargs)
transaction.on_commit(lambda: cache.delete(self.user_access_permission_key(self.user_id)))
def delete(self, *args, **kwargs):
with transaction.atomic():
super().delete(*args, **kwargs)
transaction.on_commit(lambda: cache.delete(self.user_access_permission_key(self.user_id)))
2017-10-24 22:13:53 +02:00
class AccessRestrictionMixin(SerializableMixin, models.Model):
access_restriction = models.ForeignKey(AccessRestriction, null=True, blank=True,
verbose_name=_('Access Restriction'), on_delete=models.PROTECT)
class Meta:
abstract = True
def _serialize(self, **kwargs):
result = super()._serialize(**kwargs)
result['access_restriction'] = self.access_restriction_id
return result
2017-11-02 13:35:58 +01:00
def details_display(self):
result = super().details_display()
result['display'].extend([
(_('Access Restriction'), self.access_restriction_id and self.access_restriction.title),
2017-11-02 13:35:58 +01:00
])
return result
@classmethod
2017-08-06 22:07:34 +02:00
def q_for_request(cls, request, prefix='', allow_none=False):
if request is None and allow_none:
return Q()
2017-10-24 22:45:57 +02:00
return (Q(**{prefix+'access_restriction__isnull': True}) |
Q(**{prefix+'access_restriction__pk__in': AccessPermission.get_for_request(request)}))