much stuff for the access control panel
This commit is contained in:
parent
c9299b3cf3
commit
f13fb0a899
12 changed files with 352 additions and 8 deletions
52
src/c3nav/access/forms.py
Normal file
52
src/c3nav/access/forms.py
Normal file
|
@ -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
|
27
src/c3nav/access/migrations/0003_auto_20161221_2311.py
Normal file
27
src/c3nav/access/migrations/0003_auto_20161221_2311.py
Normal file
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
<div class="container" id="main">
|
||||
<h1>c3nav access control</h1>
|
||||
{% block nav %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% load i18n %}
|
||||
|
||||
<a href="{{ token.activation_url }}" class="btn btn-default btn-block">{% trans 'Activate token on this device' %}</a>
|
||||
<p>
|
||||
<a href="{{ token.activation_url }}" class="btn btn-default btn-block">{% trans 'Activate token on this device' %}</a>
|
||||
</p>
|
||||
|
|
10
src/c3nav/access/templates/access/loggedin_base.html
Normal file
10
src/c3nav/access/templates/access/loggedin_base.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends 'access/base.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block nav %}
|
||||
<ul class="nav nav-pills">
|
||||
<li class="active"><a href="{% url 'access.users' %}">{% trans 'Users' %}</a></li>
|
||||
<li><a href="{% url 'access.logout' %}">{% trans 'Log out' %}</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
83
src/c3nav/access/templates/access/user.html
Normal file
83
src/c3nav/access/templates/access/user.html
Normal file
|
@ -0,0 +1,83 @@
|
|||
|
||||
|
||||
|
||||
{% extends 'access/loggedin_base.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Access Tokens <small>{{ user.user_url }}</small></h3>
|
||||
<p>{{ user.description }}</p>
|
||||
<form method="POST">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h4>{% trans 'Add new access token' %}</h4>
|
||||
<div class="row">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form new_token_form form_group_class='form-group col-md-3' %}
|
||||
<div class="form-group col-md-3">
|
||||
<label class="control-label"> </label>
|
||||
<button type="submit" name="create" class="btn btn-primary btn-block btn-sm">{% trans 'Add' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'ID' %}</th>
|
||||
<th>{% trans 'Description' %}</th>
|
||||
<th>{% trans 'Author' %}</th>
|
||||
<th>{% trans 'Permissions' %}</th>
|
||||
<th>{% trans 'Creation Date' %}</th>
|
||||
<th>{% trans 'Expiration' %}</th>
|
||||
<th>{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for token in tokens %}
|
||||
<tr>
|
||||
<td>{{ token.id }}</td>
|
||||
<td>{{ token.description }}</td>
|
||||
<td>{% if token.author %}{{ token.author }}{% endif %}</td>
|
||||
<td>
|
||||
{% if token.full_access %}
|
||||
<span class="text-success">all</span>
|
||||
{% else %}
|
||||
{% for location in token.permissions_list_objects %}
|
||||
{{ location.title }},
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ token.creation_date|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>
|
||||
{% if token.is_expired %}
|
||||
{% trans 'expired' %}
|
||||
{% elif token.expires %}
|
||||
{{ token.expires|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
{% trans 'never' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not token.is_expired %}
|
||||
<button class="btn btn-xs btn-danger" type="submit" name="expire" value="{{ token.id }}">{% trans 'Expire' %}</button>
|
||||
{% if not token.activated or 1 %}
|
||||
<a href="{% url 'access.user.token' user=user.id token=token.id %}" class="btn btn-xs btn-success" type="submit" name="activate" value="{{ token.id }}">{% trans 'Activate' %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
13
src/c3nav/access/templates/access/user_token.html
Normal file
13
src/c3nav/access/templates/access/user_token.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends 'access/loggedin_base.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
{% include 'access/fragment_token.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<p><a href="{% url 'access.user' pk=user.id %}">{% trans 'back to user' %}</a></p>
|
||||
{% endblock %}
|
54
src/c3nav/access/templates/access/users.html
Normal file
54
src/c3nav/access/templates/access/users.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
{% extends 'access/loggedin_base.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Users</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'ID' %}</th>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Author' %}</th>
|
||||
<th>{% trans 'Description' %}</th>
|
||||
<th>{% trans 'Active Tokens' %}</th>
|
||||
<th>{% trans 'Creation Date' %}</th>
|
||||
<th>{% trans 'Details' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.user_url }}</td>
|
||||
<td>{% if user.author %}{{ user.author }}{% endif %}</td>
|
||||
<td>{{ user.description }}</td>
|
||||
<td>{{ user.valid_tokens.count }}</td>
|
||||
<td>{{ user.creation_date|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td><a href="{% url 'access.user' pk=user.pk %}">{% trans 'Details' %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ul class="pager">
|
||||
{% if users.has_previous %}
|
||||
<li class="previous"><a href="{% url 'acces.users' page=users.previous_page_number %}">« {% trans 'previous' %}</a></li>
|
||||
{% else %}
|
||||
<li class="previous disabled"><a href="#">« {% trans 'previous' %}</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li class="middle">
|
||||
{% blocktrans with number=users.number total=users.paginator.num_pages %}
|
||||
Page {{ number }} of {{ total }}
|
||||
{% endblocktrans %}
|
||||
</li>
|
||||
|
||||
{% if users.has_next %}
|
||||
<li class="next"><a href="{% url 'acces.users' page=users.next_page_number %}">{% trans 'next' %} »</a></li>
|
||||
{% else %}
|
||||
<li class="next disabled"><a href="#">{% trans 'next' %} »</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
|
@ -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<pk>[0-9]+):(?P<secret>[a-zA-Z0-9]+)/$', activate_token, name='access.activate'),
|
||||
url(r'^users/$', user_list, name='access.users'),
|
||||
url(r'^users/(?P<page>[0-9]+)/$', user_list, name='access.users'),
|
||||
url(r'^user/(?P<pk>[0-9]+)/$', user_detail, name='access.user'),
|
||||
url(r'^user/(?P<user>[0-9]+)/(?P<token>[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'),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue