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
|
||||
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
|
||||
self.author_access_permissions = AccessPermission.get_for_request_with_expire_date(request, can_grant=True)
|
||||
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.')
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from contextlib import suppress
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -22,8 +23,9 @@ def grant_access(request): # todo: make class based view
|
|||
token = form.get_token()
|
||||
token.save()
|
||||
if settings.DEBUG:
|
||||
signed_data = form.get_signed_data()
|
||||
print('/?'+urlencode({'access': signed_data}))
|
||||
with suppress(ValueError):
|
||||
signed_data = form.get_signed_data()
|
||||
print('/?'+urlencode({'access': signed_data}))
|
||||
return redirect(reverse('control.access.qr', kwargs={'token': token.token}))
|
||||
else:
|
||||
form = AccessPermissionForm(request=request)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,16 +68,21 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N
|
|||
if access_token:
|
||||
with transaction.atomic():
|
||||
try:
|
||||
token = AccessPermissionToken.objects.select_for_update().get(token=access_token, redeemed=False,
|
||||
valid_until__gte=timezone.now())
|
||||
except AccessPermissionToken.DoesNotExist:
|
||||
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, ValueError, ValidationError, SignedPermissionDataError):
|
||||
messages.error(request, _('This token does not exist or was already redeemed.'))
|
||||
else:
|
||||
num_restrictions = len(token.restrictions)
|
||||
with transaction.atomic():
|
||||
token.save()
|
||||
if token.pk:
|
||||
token.save()
|
||||
token.redeem(request=request)
|
||||
token.save()
|
||||
if token.pk:
|
||||
token.save()
|
||||
|
||||
if request.user.is_authenticated:
|
||||
messages.success(request, ngettext_lazy('Area successfully unlocked.',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue