From eb54ac7896c6b94ce64a0bb1954b12892b2ad949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 8 Dec 2017 21:31:53 +0100 Subject: [PATCH] manage access permissions --- src/c3nav/control/forms.py | 134 +++++++++++++++++- src/c3nav/control/templates/control/user.html | 25 +++- src/c3nav/control/views.py | 24 +++- .../0055_grant_access_permissions.py | 28 ++++ src/c3nav/mapdata/models/access.py | 6 +- src/c3nav/site/static/site/css/c3nav.css | 2 + 6 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0055_grant_access_permissions.py diff --git a/src/c3nav/control/forms.py b/src/c3nav/control/forms.py index 01c931a2..5f950aa0 100644 --- a/src/c3nav/control/forms.py +++ b/src/c3nav/control/forms.py @@ -1,9 +1,141 @@ -from django.forms import ModelForm +import time +import uuid +from datetime import timedelta +from itertools import chain + +from django.core.cache import cache +from django.db import transaction +from django.db.models import Q +from django.forms import BooleanField, ChoiceField, Form, ModelForm +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy from c3nav.control.models import UserPermissions +from c3nav.mapdata.models.access import AccessPermission, AccessRestriction class UserPermissionsForm(ModelForm): class Meta: model = UserPermissions exclude = ('user', ) + + +class AccessPermissionForm(Form): + def __init__(self, request, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.author = request.user + + if not request.user_permissions.access_all: + self.author_access_permissions = { + pk: expire_date for pk, expire_date in self.author.accesspermissions.filter( + Q(can_grant=True) & (Q(expire_date__isnull=True) | Q(expire_date__lt=timezone.now())) + ).values_list('access_restriction_id', 'expire_date') + } + access_restrictions = AccessRestriction.objects.filter( + pk__in=self.author_access_permissions.keys() + ) + else: + self.author_access_permissions = {} + access_restrictions = AccessRestriction.objects.all() + + self.access_restrictions = { + access_restriction.pk: access_restriction + for access_restriction in access_restrictions + } + + self.access_restriction_choices = { + 'all': self.access_restrictions.values(), + **{str(pk): (access_restriction, ) for pk, access_restriction in self.access_restrictions.items()} + } + + choices = [('', _('choose permissions…')), + ('all', ungettext_lazy('everything possible (%d permission)', + 'everything possible (%d permissions)', + len(access_restrictions)) % len(access_restrictions))] + + choices.append((_('Access Permissions'), tuple( + (str(pk), access_restriction.title) + for pk, access_restriction in self.access_restrictions.items() + ))) + + self.fields['access_restrictions'] = ChoiceField(label=_('Access Permission'), + choices=choices, required=True) + + expire_choices = [ + ('', _('never')), + ] + for minutes in range(15, 60, 15): + expire_choices.append( + (str(minutes), ungettext_lazy('in %d minute', 'in %d minutes', minutes) % minutes)) + + for hours in chain(range(1, 6), range(6, 24, 6)): + expire_choices.append( + (str(hours*60), ungettext_lazy('in %d hour', 'in %d hours', hours) % hours) + ) + expire_choices.insert( + 5, (str(90), _('in 1½ hour')) + ) + for days in range(1, 14): + expire_choices.append( + (str(days*24*60), ungettext_lazy('in %d day', 'in %d days', days) % days) + ) + + self.fields['expires'] = ChoiceField(label=_('expires'), required=False, initial='60', + choices=expire_choices) + + if request.user_permissions.access_all: + self.fields['can_grant'] = BooleanField(label=_('can grant'), required=False) + + def clean_access_restrictions(self): + data = self.cleaned_data['access_restrictions'] + return self.access_restriction_choices[data] + + def clean_expires(self): + data = self.cleaned_data['expires'] + if data == '': + return None + return timezone.now()+timedelta(minutes=int(data)) + + def save(self, user): + self._save_code(self._create_code(), user) + + def create_code(self, timeout=30): + code = uuid.uuid4() + cache.set('access:code:%s' % code, (self._create_code(), time.time()+timeout), timeout) + + def save_code(self, code, user): + cache_key = 'access:code:%s' % code + with transaction.atomic(): + AccessPermission.objects.select_for_update().first() + code, expires = cache.get(cache_key, (None, None)) + if code is None or expires < time.time(): + raise ValueError + self._save_code(code, user) + cache.delete(cache_key) + + def _create_code(self): + restrictions = [] + for restriction in self.cleaned_data['access_restrictions']: + expires = self.cleaned_data['expires'] + author_expires = self.author_access_permissions.get(restriction.pk) + if author_expires is not None: + expires = author_expires if expires is None else min(expires, author_expires) + restrictions.append((restriction.pk, expires)) + return (tuple(restrictions), self.author.pk, self.cleaned_data.get('can_grant', False)) + + @classmethod + def _save_code(cls, code, user): + restrictions, author_id, can_grant = code + print(code) + with transaction.atomic(): + for pk, expire_date in restrictions: + obj, created = AccessPermission.objects.get_or_create( + user=user, + access_restriction_id=pk + ) + obj.author_id = author_id + obj.expire_date = expire_date + obj.can_grant = can_grant + obj.save() diff --git a/src/c3nav/control/templates/control/user.html b/src/c3nav/control/templates/control/user.html index a311d055..6f9a8453 100644 --- a/src/c3nav/control/templates/control/user.html +++ b/src/c3nav/control/templates/control/user.html @@ -9,7 +9,7 @@
{% csrf_token %} {% for field in user_permissions_form %} - +
{% endfor %}
@@ -22,4 +22,27 @@ {% endfor %}

{% endif %} + +

{% trans 'Access Permissions' %}

+ {% if user.accesspermissions.all %} + + + + + + {% for access_permission in user.accesspermissions.all %} + + + + + {% endfor %} +
{% trans 'Access Restriction' %}{% trans 'expires' %}
{{ access_permission.access_restriction.title }}{{ access_permission.expire_date }}
+ {% else %} +

{% trans 'none' %}

+ {% endif %} +
+ {% csrf_token %} + {{ add_access_permission_form }} + +
{% endblock %} diff --git a/src/c3nav/control/views.py b/src/c3nav/control/views.py index 213fc28c..7868f857 100644 --- a/src/c3nav/control/views.py +++ b/src/c3nav/control/views.py @@ -5,11 +5,13 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator +from django.db.models import Prefetch from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import ugettext_lazy as _ -from c3nav.control.forms import UserPermissionsForm +from c3nav.control.forms import AccessPermissionForm, UserPermissionsForm from c3nav.control.models import UserPermissions +from c3nav.mapdata.models.access import AccessPermission def control_panel_view(func): @@ -48,7 +50,11 @@ def user_list(request): @login_required @control_panel_view def user_detail(request, user): - qs = User.objects.select_related('permissions').prefetch_related('accesspermissions') + qs = User.objects.select_related( + 'permissions', + ).prefetch_related( + Prefetch('accesspermissions', AccessPermission.objects.select_related('access_restriction')) + ) user = get_object_or_404(qs, pk=user) ctx = { @@ -79,4 +85,18 @@ def user_detail(request, user): 'user_permissions_form': form }) + # access permissions + if request.method == 'POST' and request.POST.get('submit_access_permissions'): + form = AccessPermissionForm(request=request, data=request.POST) + if form.is_valid(): + form.save(user) + messages.success(request, _('Access permissions successfully updated.')) + return redirect(request.path_info) + else: + form = AccessPermissionForm(request=request) + + ctx.update({ + 'add_access_permission_form': form + }) + return render(request, 'control/user.html', ctx) diff --git a/src/c3nav/mapdata/migrations/0055_grant_access_permissions.py b/src/c3nav/mapdata/migrations/0055_grant_access_permissions.py new file mode 100644 index 00000000..8bd822b3 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0055_grant_access_permissions.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-12-08 18:23 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('mapdata', '0054_title_plural'), + ] + + operations = [ + migrations.AddField( + model_name='accesspermission', + name='author', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='authored_access_permissions', to=settings.AUTH_USER_MODEL, verbose_name='Author'), + ), + migrations.AddField( + model_name='accesspermission', + name='can_grant', + field=models.BooleanField(default=False, verbose_name='can grant'), + ), + ] diff --git a/src/c3nav/mapdata/models/access.py b/src/c3nav/mapdata/models/access.py index c5b8b162..44d51fcc 100644 --- a/src/c3nav/mapdata/models/access.py +++ b/src/c3nav/mapdata/models/access.py @@ -15,7 +15,8 @@ class AccessRestriction(TitledMixin, models.Model): """ An access restriction """ - users = models.ManyToManyField(settings.AUTH_USER_MODEL, through='AccessPermission') + users = models.ManyToManyField(settings.AUTH_USER_MODEL, through='AccessPermission', + through_fields=('access_restriction', 'user')) open = models.BooleanField(default=False, verbose_name=_('open')) class Meta: @@ -34,6 +35,9 @@ 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')) + 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')) class Meta: verbose_name = _('Access Permission') diff --git a/src/c3nav/site/static/site/css/c3nav.css b/src/c3nav/site/static/site/css/c3nav.css index 4a6173b8..dcb6e66a 100644 --- a/src/c3nav/site/static/site/css/c3nav.css +++ b/src/c3nav/site/static/site/css/c3nav.css @@ -703,4 +703,6 @@ main.control h4 { .user-permissions-form label { font-weight: 400; + width: auto; + display: inline; }