fixed redemption of signed access tokens
This commit is contained in:
parent
4b71893198
commit
168a6a5af6
4 changed files with 92 additions and 14 deletions
|
@ -40,12 +40,15 @@ class AccessPermissionForm(Form):
|
||||||
|
|
||||||
# remember author if this form is saved
|
# remember author if this form is saved
|
||||||
self.author = author or request.user
|
self.author = author or request.user
|
||||||
author_permissions = request.user_permissions if request else author.permissions
|
author_permissions = request.user_permissions if request else UserPermissions.get_for_user(author)
|
||||||
|
|
||||||
self.expire_date = expire_date
|
self.expire_date = expire_date
|
||||||
|
|
||||||
# determine which access permissions the author can grant
|
# determine which access permissions the author can grant
|
||||||
|
if request:
|
||||||
self.author_access_permissions = AccessPermission.get_for_request_with_expire_date(request, can_grant=True)
|
self.author_access_permissions = AccessPermission.get_for_request_with_expire_date(request, can_grant=True)
|
||||||
|
else:
|
||||||
|
self.author_access_permissions = AccessPermission.get_for_user_with_expire_date(author, can_grant=True)
|
||||||
|
|
||||||
access_restrictions = AccessRestriction.objects.filter(
|
access_restrictions = AccessRestriction.objects.filter(
|
||||||
pk__in=self.author_access_permissions.keys()
|
pk__in=self.author_access_permissions.keys()
|
||||||
|
@ -62,7 +65,11 @@ class AccessPermissionForm(Form):
|
||||||
}
|
}
|
||||||
|
|
||||||
# get access permission groups
|
# get access permission groups
|
||||||
groups = AccessRestrictionGroup.qs_for_request(request).prefetch_related(
|
if request:
|
||||||
|
groups = AccessRestrictionGroup.qs_for_request(request)
|
||||||
|
else:
|
||||||
|
groups = AccessRestrictionGroup.qs_for_user(author)
|
||||||
|
groups = groups.prefetch_related(
|
||||||
Prefetch('accessrestrictions', AccessRestriction.objects.only('pk'))
|
Prefetch('accessrestrictions', AccessRestriction.objects.only('pk'))
|
||||||
)
|
)
|
||||||
self.group_contents: dict[int, set[int]] = {
|
self.group_contents: dict[int, set[int]] = {
|
||||||
|
@ -199,7 +206,7 @@ class AccessPermissionForm(Form):
|
||||||
data = {
|
data = {
|
||||||
'id': self.data['access_restrictions'],
|
'id': self.data['access_restrictions'],
|
||||||
'time': int(time.time()),
|
'time': int(time.time()),
|
||||||
'valid_until': int(self.cleaned_data['expires'].strftime('%s')),
|
'valid_until': int(self.cleaned_data['expires'].strftime('%s')) if self.cleaned_data['expires'] else None,
|
||||||
'author': self.author.pk,
|
'author': self.author.pk,
|
||||||
}
|
}
|
||||||
if key is not None:
|
if key is not None:
|
||||||
|
@ -265,7 +272,7 @@ class AccessPermissionForm(Form):
|
||||||
raise SignedPermissionDataError('Author does not exist.')
|
raise SignedPermissionDataError('Author does not exist.')
|
||||||
|
|
||||||
api_secrets = author.api_secrets.filter(
|
api_secrets = author.api_secrets.filter(
|
||||||
scope_grant_permission=True
|
scope_grant_permissions=True
|
||||||
).valid_only().values_list('api_secret', flat=True)
|
).valid_only().values_list('api_secret', flat=True)
|
||||||
if not api_secrets:
|
if not api_secrets:
|
||||||
raise SignedPermissionDataError('Author has no API secret.')
|
raise SignedPermissionDataError('Author has no API secret.')
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from contextlib import suppress
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -22,6 +23,7 @@ def grant_access(request): # todo: make class based view
|
||||||
token = form.get_token()
|
token = form.get_token()
|
||||||
token.save()
|
token.save()
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
with suppress(ValueError):
|
||||||
signed_data = form.get_signed_data()
|
signed_data = form.get_signed_data()
|
||||||
print('/?'+urlencode({'access': signed_data}))
|
print('/?'+urlencode({'access': signed_data}))
|
||||||
return redirect(reverse('control.access.qr', kwargs={'token': token.token}))
|
return redirect(reverse('control.access.qr', kwargs={'token': token.token}))
|
||||||
|
|
|
@ -84,6 +84,20 @@ class AccessRestrictionGroup(TitledMixin, models.Model):
|
||||||
filter_perms = all_permissions - permissions
|
filter_perms = all_permissions - permissions
|
||||||
return ~Q(accessrestrictions__pk__in=filter_perms)
|
return ~Q(accessrestrictions__pk__in=filter_perms)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def qs_for_user(cls, user):
|
||||||
|
return cls.objects.filter(cls.q_for_user(user))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def q_for_user(cls, user):
|
||||||
|
if user.is_authenticated and user.is_superuser:
|
||||||
|
return Q()
|
||||||
|
all_permissions = AccessRestriction.get_all()
|
||||||
|
permissions = AccessPermission.get_for_user(user)
|
||||||
|
# now we filter out groups where the user doesn't have a permission for all members
|
||||||
|
filter_perms = all_permissions - permissions
|
||||||
|
return ~Q(accessrestrictions__pk__in=filter_perms)
|
||||||
|
|
||||||
|
|
||||||
def default_valid_until():
|
def default_valid_until():
|
||||||
return timezone.now()+timedelta(seconds=20)
|
return timezone.now()+timedelta(seconds=20)
|
||||||
|
@ -134,7 +148,7 @@ class AccessPermissionToken(models.Model):
|
||||||
"session_token": request.session.setdefault("accesspermission_session_token", str(uuid.uuid4()))
|
"session_token": request.session.setdefault("accesspermission_session_token", str(uuid.uuid4()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (grant_to is None and self.redeemed) or (self.accesspermissions.exists() and not self.unlimited):
|
if (grant_to is None and self.redeemed) or (self.pk and self.accesspermissions.exists() and not self.unlimited):
|
||||||
raise self.RedeemError('Already redeemed.')
|
raise self.RedeemError('Already redeemed.')
|
||||||
|
|
||||||
if timezone.now() > self.valid_until + timedelta(minutes=5 if self.redeemed else 0):
|
if timezone.now() > self.valid_until + timedelta(minutes=5 if self.redeemed else 0):
|
||||||
|
@ -294,6 +308,55 @@ class AccessPermission(models.Model):
|
||||||
cache.set(cache_key, access_restriction_ids, max(0.0, (expire_date - timezone.now()).total_seconds()))
|
cache.set(cache_key, access_restriction_ids, max(0.0, (expire_date - timezone.now()).total_seconds()))
|
||||||
return set(access_restriction_ids) | AccessRestriction.get_all_public()
|
return set(access_restriction_ids) | AccessRestriction.get_all_public()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_user_with_expire_date(cls, user, can_grant=None):
|
||||||
|
from c3nav.control.models import UserPermissions
|
||||||
|
if UserPermissions.get_for_user(user).grant_all_access:
|
||||||
|
return {pk: None for pk in AccessRestriction.get_all()}
|
||||||
|
qs = cls.queryset_for_user(user, can_grant)
|
||||||
|
|
||||||
|
result = tuple(
|
||||||
|
qs.select_related(
|
||||||
|
'access_restriction_group'
|
||||||
|
).prefetch_related('access_restriction_group__accessrestrictions')
|
||||||
|
)
|
||||||
|
|
||||||
|
# collect permissions (can be multiple for one restriction)
|
||||||
|
permissions = {}
|
||||||
|
for permission in result:
|
||||||
|
if permission.access_restriction_id:
|
||||||
|
permissions.setdefault(permission.access_restriction_id, set()).add(permission.expire_date)
|
||||||
|
if permission.access_restriction_group_id:
|
||||||
|
for member in permission.access_restriction_group.accessrestrictions.all():
|
||||||
|
permissions.setdefault(member.pk, set()).add(permission.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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_user(cls, user) -> set[int]:
|
||||||
|
from c3nav.control.models import UserPermissions
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return AccessRestriction.get_all_public()
|
||||||
|
|
||||||
|
if UserPermissions.get_for_user(user).grant_all_access:
|
||||||
|
return AccessRestriction.get_all()
|
||||||
|
|
||||||
|
cache_key = cls.build_access_permission_key(user_id=user.pk)
|
||||||
|
access_restriction_ids = cache.get(cache_key, None)
|
||||||
|
if access_restriction_ids is None or True:
|
||||||
|
permissions = cls.get_for_user_with_expire_date(user)
|
||||||
|
|
||||||
|
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.0, (expire_date-timezone.now()).total_seconds()))
|
||||||
|
return set(access_restriction_ids) | AccessRestriction.get_all_public()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cache_key_for_request(cls, request, with_update=True):
|
def cache_key_for_request(cls, request, with_update=True):
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.contrib.auth import login, logout
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
|
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation
|
from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation, ValidationError
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
@ -27,6 +27,7 @@ from django.views.decorators.http import etag
|
||||||
|
|
||||||
from c3nav import __version__ as c3nav_version
|
from c3nav import __version__ as c3nav_version
|
||||||
from c3nav.api.models import Secret
|
from c3nav.api.models import Secret
|
||||||
|
from c3nav.control.forms import AccessPermissionForm, SignedPermissionDataError
|
||||||
from c3nav.mapdata.grid import grid
|
from c3nav.mapdata.grid import grid
|
||||||
from c3nav.mapdata.models import Location, Source
|
from c3nav.mapdata.models import Location, Source
|
||||||
from c3nav.mapdata.models.access import AccessPermission, AccessPermissionToken
|
from c3nav.mapdata.models.access import AccessPermission, AccessPermissionToken
|
||||||
|
@ -67,15 +68,20 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N
|
||||||
if access_token:
|
if access_token:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
|
if ':' in access_token:
|
||||||
|
token = AccessPermissionForm.load_signed_data(access_token)
|
||||||
|
else:
|
||||||
token = AccessPermissionToken.objects.select_for_update().get(token=access_token, redeemed=False,
|
token = AccessPermissionToken.objects.select_for_update().get(token=access_token, redeemed=False,
|
||||||
valid_until__gte=timezone.now())
|
valid_until__gte=timezone.now())
|
||||||
except AccessPermissionToken.DoesNotExist:
|
except (AccessPermissionToken.DoesNotExist, ValueError, ValidationError, SignedPermissionDataError):
|
||||||
messages.error(request, _('This token does not exist or was already redeemed.'))
|
messages.error(request, _('This token does not exist or was already redeemed.'))
|
||||||
else:
|
else:
|
||||||
num_restrictions = len(token.restrictions)
|
num_restrictions = len(token.restrictions)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
if token.pk:
|
||||||
token.save()
|
token.save()
|
||||||
token.redeem(request=request)
|
token.redeem(request=request)
|
||||||
|
if token.pk:
|
||||||
token.save()
|
token.save()
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue