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
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
# 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)
else:
self.author_access_permissions = AccessPermission.get_for_user_with_expire_date(author, can_grant=True)
access_restrictions = AccessRestriction.objects.filter(
pk__in=self.author_access_permissions.keys()
@ -62,7 +65,11 @@ class AccessPermissionForm(Form):
}
# 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'))
)
self.group_contents: dict[int, set[int]] = {
@ -199,7 +206,7 @@ class AccessPermissionForm(Form):
data = {
'id': self.data['access_restrictions'],
'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,
}
if key is not None:
@ -265,7 +272,7 @@ class AccessPermissionForm(Form):
raise SignedPermissionDataError('Author does not exist.')
api_secrets = author.api_secrets.filter(
scope_grant_permission=True
scope_grant_permissions=True
).valid_only().values_list('api_secret', flat=True)
if not api_secrets:
raise SignedPermissionDataError('Author has no API secret.')

View file

@ -1,3 +1,4 @@
from contextlib import suppress
from urllib.parse import urlencode
from django.conf import settings
@ -22,6 +23,7 @@ def grant_access(request): # todo: make class based view
token = form.get_token()
token.save()
if settings.DEBUG:
with suppress(ValueError):
signed_data = form.get_signed_data()
print('/?'+urlencode({'access': signed_data}))
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
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():
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()))
}
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.')
if timezone.now() > self.valid_until + timedelta(minutes=5 if self.redeemed else 0):
@ -290,6 +304,55 @@ class AccessPermission(models.Model):
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
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()

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.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
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.serializers.json import DjangoJSONEncoder
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.api.models import Secret
from c3nav.control.forms import AccessPermissionForm, SignedPermissionDataError
from c3nav.mapdata.grid import grid
from c3nav.mapdata.models import Location, Source
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:
with transaction.atomic():
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,
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.'))
else:
num_restrictions = len(token.restrictions)
with transaction.atomic():
if token.pk:
token.save()
token.redeem(request=request)
if token.pk:
token.save()
if request.user.is_authenticated: