From f13fb0a8993c14df6848927d80fd939068d6963c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 22 Dec 2016 01:57:35 +0100 Subject: [PATCH] much stuff for the access control panel --- src/c3nav/access/forms.py | 52 ++++++++++++ .../migrations/0003_auto_20161221_2311.py | 27 ++++++ src/c3nav/access/models.py | 22 ++++- .../access/static/access/css/c3nav-access.css | 13 +++ src/c3nav/access/templates/access/base.html | 3 + .../templates/access/fragment_token.html | 4 +- .../templates/access/loggedin_base.html | 10 +++ src/c3nav/access/templates/access/user.html | 83 +++++++++++++++++++ .../access/templates/access/user_token.html | 13 +++ src/c3nav/access/templates/access/users.html | 54 ++++++++++++ src/c3nav/access/urls.py | 6 +- src/c3nav/access/views.py | 73 +++++++++++++++- 12 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 src/c3nav/access/forms.py create mode 100644 src/c3nav/access/migrations/0003_auto_20161221_2311.py create mode 100644 src/c3nav/access/templates/access/loggedin_base.html create mode 100644 src/c3nav/access/templates/access/user.html create mode 100644 src/c3nav/access/templates/access/user_token.html create mode 100644 src/c3nav/access/templates/access/users.html diff --git a/src/c3nav/access/forms.py b/src/c3nav/access/forms.py new file mode 100644 index 00000000..f3e47d18 --- /dev/null +++ b/src/c3nav/access/forms.py @@ -0,0 +1,52 @@ +from django.forms import ModelForm, MultipleChoiceField +from django.utils.translation import ugettext_lazy as _ + +from c3nav.access.models import AccessToken, AccessUser +from c3nav.mapdata.models import AreaLocation + + +class AccessUserForm(ModelForm): + class Meta: + model = AccessUser + fields = ['user_url', 'description'] + + +class AccessTokenForm(ModelForm): + def __init__(self, *args, request, **kwargs): + super().__init__(*args, **kwargs) + locations = AreaLocation.objects.filter(routing_inclusion='needs_permission') + + has_operator = True + try: + request.user.operator + except: + has_operator = False + + OPTIONS = [] + can_full = False + if request.user.is_superuser: + can_full = True + elif has_operator: + can_award = request.user.operator.can_award_permissions.split(';') + can_full = ':full' in can_award + locations = locations.filter(name__in=can_award) + else: + locations = [] + + if can_full: + OPTIONS.append((':full', _('Full Permissions'))) + + OPTIONS += [(location.name, location.title) for location in locations] + print(OPTIONS) + self.fields['permissions'] = MultipleChoiceField(choices=OPTIONS, required=True) + + class Meta: + model = AccessToken + fields = ['permissions', 'description', 'expires'] + + def clean_permissions(self): + data = self.cleaned_data['permissions'] + if ':full' in data: + data = [':full'] + data = ';'.join(data) + return data diff --git a/src/c3nav/access/migrations/0003_auto_20161221_2311.py b/src/c3nav/access/migrations/0003_auto_20161221_2311.py new file mode 100644 index 00000000..7815884d --- /dev/null +++ b/src/c3nav/access/migrations/0003_auto_20161221_2311.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-21 23:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0002_auto_20161221_1739'), + ] + + operations = [ + migrations.AlterField( + model_name='accessoperator', + name='description', + field=models.TextField(blank=True, default='', verbose_name='description'), + preserve_default=False, + ), + migrations.AlterField( + model_name='accessuser', + name='description', + field=models.TextField(blank=True, default='', max_length=200, verbose_name='description'), + preserve_default=False, + ), + ] diff --git a/src/c3nav/access/models.py b/src/c3nav/access/models.py index 3e47723a..dc0214cd 100644 --- a/src/c3nav/access/models.py +++ b/src/c3nav/access/models.py @@ -10,10 +10,12 @@ from django.utils.crypto import get_random_string from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ +from c3nav.mapdata.models import AreaLocation + class AccessOperator(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='operator') - description = models.TextField(_('description'), null=True, blank=True) + description = models.TextField(_('description'), blank=True) can_award_permissions = models.CharField(_('can award permissions'), max_length=2048) access_from = models.DateTimeField(_('has access from'), null=True, blank=True) access_until = models.DateTimeField(_('has access until'), null=True, blank=True) @@ -31,7 +33,7 @@ class AccessUser(models.Model): help_text=_('Usually an URL to a profile somewhere')) author = models.ForeignKey(AccessOperator, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_('creator')) - description = models.TextField(_('description'), max_length=200, null=True, blank=True) + description = models.TextField(_('description'), max_length=200, blank=True) creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) class Meta: @@ -43,7 +45,7 @@ class AccessUser(models.Model): return self.tokens.filter(Q(expired=False) | Q(expires__isnull=False, expires__lt=timezone.now())) def new_token(self, **kwargs): - kwargs['secret'] = get_random_string(42, string.ascii_letters + string.digits) + kwargs['secret'] = AccessToken.create_secret() return self.tokens.create(**kwargs) def __str__(self): @@ -70,16 +72,28 @@ class AccessToken(models.Model): def permissions_list(self): return self.permissions.split(';') + @cached_property + def permissions_list_objects(self): + return AreaLocation.objects.filter(name__in=self.permissions_list) + @cached_property def full_access(self): return ':full' in self.permissions_list + @property + def is_expired(self): + return self.expired or (self.expires is not None and self.expires < timezone.now()) + @property def activation_url(self): if self.activated: return None return reverse('access.activate', kwargs={'pk': self.pk, 'secret': self.secret}) + @staticmethod + def create_secret(): + return get_random_string(42, string.ascii_letters + string.digits) + def new_instance(self): with transaction.atomic(): for instance in self.instances.filter(expires__isnull=True): @@ -88,7 +102,7 @@ class AccessToken(models.Model): self.instances.filter(expires__isnull=False, expires__lt=timezone.now()).delete() - secret = get_random_string(42, string.ascii_letters+string.digits) + secret = self.create_secret() self.instances.create(secret=secret) return '%d:%s' % (self.pk, secret) diff --git a/src/c3nav/access/static/access/css/c3nav-access.css b/src/c3nav/access/static/access/css/c3nav-access.css index b765dd68..ea6ebba9 100644 --- a/src/c3nav/access/static/access/css/c3nav-access.css +++ b/src/c3nav/access/static/access/css/c3nav-access.css @@ -9,6 +9,10 @@ h1 { .login .container { max-width:420px; } +.pager .middle { + top:3px; + position:relative; +} footer { @@ -25,3 +29,12 @@ footer { border-width:0 !important; padding:0; } + + +.table .btn { + font-size:14px; +} +.table tbody tr td { + min-height: 29px; + line-height:29px; +} diff --git a/src/c3nav/access/templates/access/base.html b/src/c3nav/access/templates/access/base.html index 71060762..1ed95c6c 100644 --- a/src/c3nav/access/templates/access/base.html +++ b/src/c3nav/access/templates/access/base.html @@ -17,6 +17,9 @@

c3nav access control

+ {% block nav %} + {% endblock %} + {% block content %} {% endblock %} diff --git a/src/c3nav/access/templates/access/fragment_token.html b/src/c3nav/access/templates/access/fragment_token.html index 87cece39..a71dc293 100644 --- a/src/c3nav/access/templates/access/fragment_token.html +++ b/src/c3nav/access/templates/access/fragment_token.html @@ -1,3 +1,5 @@ {% load i18n %} -{% trans 'Activate token on this device' %} +

+ {% trans 'Activate token on this device' %} +

diff --git a/src/c3nav/access/templates/access/loggedin_base.html b/src/c3nav/access/templates/access/loggedin_base.html new file mode 100644 index 00000000..c551ed29 --- /dev/null +++ b/src/c3nav/access/templates/access/loggedin_base.html @@ -0,0 +1,10 @@ +{% extends 'access/base.html' %} + +{% load i18n %} + +{% block nav %} + +{% endblock %} diff --git a/src/c3nav/access/templates/access/user.html b/src/c3nav/access/templates/access/user.html new file mode 100644 index 00000000..a88a09e2 --- /dev/null +++ b/src/c3nav/access/templates/access/user.html @@ -0,0 +1,83 @@ + + + +{% extends 'access/loggedin_base.html' %} + +{% load bootstrap3 %} +{% load i18n %} + +{% block content %} +

Access Tokens {{ user.user_url }}

+

{{ user.description }}

+
+ +
+
+

{% trans 'Add new access token' %}

+
+ {% csrf_token %} + {% bootstrap_form new_token_form form_group_class='form-group col-md-3' %} +
+ + +
+
+
+
+ +
+
+ {% csrf_token %} + + + + + + + + + + + + + + + {% for token in tokens %} + + + + + + + + + + {% endfor %} + +
{% trans 'ID' %}{% trans 'Description' %}{% trans 'Author' %}{% trans 'Permissions' %}{% trans 'Creation Date' %}{% trans 'Expiration' %}{% trans 'Action' %}
{{ token.id }}{{ token.description }}{% if token.author %}{{ token.author }}{% endif %} + {% if token.full_access %} + all + {% else %} + {% for location in token.permissions_list_objects %} + {{ location.title }}, + {% endfor %} + {% endif %} + {{ token.creation_date|date:"SHORT_DATETIME_FORMAT" }} + {% if token.is_expired %} + {% trans 'expired' %} + {% elif token.expires %} + {{ token.expires|date:"SHORT_DATETIME_FORMAT" }} + {% else %} + {% trans 'never' %} + {% endif %} + + {% if not token.is_expired %} + + {% if not token.activated or 1 %} + {% trans 'Activate' %} + {% endif %} + {% endif %} +
+
+ +{% endblock %} diff --git a/src/c3nav/access/templates/access/user_token.html b/src/c3nav/access/templates/access/user_token.html new file mode 100644 index 00000000..60d35cd9 --- /dev/null +++ b/src/c3nav/access/templates/access/user_token.html @@ -0,0 +1,13 @@ +{% extends 'access/loggedin_base.html' %} + +{% load bootstrap3 %} +{% load i18n %} + +{% block content %} +
+
+ {% include 'access/fragment_token.html' %} +
+
+

{% trans 'back to user' %}

+{% endblock %} diff --git a/src/c3nav/access/templates/access/users.html b/src/c3nav/access/templates/access/users.html new file mode 100644 index 00000000..05d211f8 --- /dev/null +++ b/src/c3nav/access/templates/access/users.html @@ -0,0 +1,54 @@ +{% extends 'access/loggedin_base.html' %} + +{% load bootstrap3 %} +{% load i18n %} + +{% block content %} +

Users

+ + + + + + + + + + + + + + {% for user in users %} + + + + + + + + + + {% endfor %} + +
{% trans 'ID' %}{% trans 'Name' %}{% trans 'Author' %}{% trans 'Description' %}{% trans 'Active Tokens' %}{% trans 'Creation Date' %}{% trans 'Details' %}
{{ user.id }}{{ user.user_url }}{% if user.author %}{{ user.author }}{% endif %}{{ user.description }}{{ user.valid_tokens.count }}{{ user.creation_date|date:"SHORT_DATETIME_FORMAT" }}{% trans 'Details' %}
+ + +{% endblock %} diff --git a/src/c3nav/access/urls.py b/src/c3nav/access/urls.py index 3259a946..e49b4a50 100644 --- a/src/c3nav/access/urls.py +++ b/src/c3nav/access/urls.py @@ -1,12 +1,16 @@ from django.conf.urls import url from django.contrib.auth import views as auth_views -from c3nav.access.views import activate_token, dashboard, prove +from c3nav.access.views import activate_token, dashboard, prove, show_user_token, user_detail, user_list urlpatterns = [ url(r'^$', dashboard, name='access.dashboard'), url(r'^prove/$', prove, name='access.prove'), url(r'^activate/(?P[0-9]+):(?P[a-zA-Z0-9]+)/$', activate_token, name='access.activate'), + url(r'^users/$', user_list, name='access.users'), + url(r'^users/(?P[0-9]+)/$', user_list, name='access.users'), + url(r'^user/(?P[0-9]+)/$', user_detail, name='access.user'), + url(r'^user/(?P[0-9]+)/(?P[0-9]+)/$', show_user_token, name='access.user.token'), url(r'^login/$', auth_views.login, {'template_name': 'access/login.html'}, name='access.login'), url(r'^logout/$', auth_views.logout, name='access.logout'), ] diff --git a/src/c3nav/access/views.py b/src/c3nav/access/views.py index 381b3db0..5d226918 100644 --- a/src/c3nav/access/views.py +++ b/src/c3nav/access/views.py @@ -1,17 +1,19 @@ from collections import OrderedDict from django.contrib.auth.decorators import login_required +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db import transaction -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, redirect, render from c3nav.access.apply import get_nonpublic_packages +from c3nav.access.forms import AccessTokenForm from c3nav.access.models import AccessToken, AccessUser from c3nav.editor.hosters import get_hoster_for_package @login_required(login_url='/access/login/') def dashboard(request): - return render(request, 'access/dashboard.html') + return redirect('access.users') def prove(request): @@ -75,3 +77,70 @@ def activate_token(request, pk, secret): return render(request, 'access/activate.html', context={ 'success': True, }) + + +@login_required(login_url='/access/login/') +def user_list(request, page=1): + queryset = AccessUser.objects.all() + paginator = Paginator(queryset, 25) + + try: + users = paginator.page(page) + except PageNotAnInteger: + return redirect('access.users') + except EmptyPage: + return redirect('access.users') + + return render(request, 'access/users.html', { + 'users': users, + }) + + +@login_required(login_url='/access/login/') +def user_detail(request, pk): + user = get_object_or_404(AccessUser, id=pk) + + tokens = user.tokens.order_by('-creation_date') + + if request.method == 'POST': + if 'expire' in request.POST: + token = get_object_or_404(AccessToken, user=user, id=request.POST['expire']) + token.expired = True + token.save() + return redirect('access.user', pk=user.id) + + new_token_form = AccessTokenForm(data=request.POST, request=request) + if new_token_form.is_valid(): + token = new_token_form.instance + token.user = user + token.secret = AccessToken.create_secret() + + author = None + try: + author = request.user.operator + except: + pass + + token.author = author + token.save() + + return redirect('access.user.token', user=user.id, token=token.id) + else: + new_token_form = AccessTokenForm(request=request) + + return render(request, 'access/user.html', { + 'user': user, + 'new_token_form': new_token_form, + 'tokens': tokens, + }) + + +@login_required(login_url='/access/login/') +def show_user_token(request, user, token): + user = get_object_or_404(AccessUser, id=user) + token = get_object_or_404(AccessToken, user=user, id=token, activated=False) + + return render(request, 'access/user_token.html', { + 'user': user, + 'tokens': token, + })