fixed redemption of signed access tokens

This commit is contained in:
Jenny Danzmayr 2024-08-15 03:31:16 +02:00
parent 4b71893198
commit 168a6a5af6
4 changed files with 92 additions and 14 deletions

View file

@ -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.')

View file

@ -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}))

View file

@ -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 (

View file

@ -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: