rename control → access and add some more stuff

This commit is contained in:
Laura Klünder 2016-12-21 18:47:39 +01:00
parent 2fcbfe6312
commit 8026b78f42
21 changed files with 183 additions and 169 deletions

View file

@ -3,7 +3,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from c3nav.control.models import AccessOperator, AccessToken, AccessTokenInstance, AccessUser from c3nav.access.models import AccessOperator, AccessToken, AccessTokenInstance, AccessUser
class AccessOperatorInline(admin.StackedInline): class AccessOperatorInline(admin.StackedInline):
@ -29,7 +29,7 @@ admin.site.register(User, UserAdmin)
class AccessTokenInline(admin.TabularInline): class AccessTokenInline(admin.TabularInline):
model = AccessToken model = AccessToken
show_change_link = True show_change_link = True
readonly_fields = ('author', 'permissions', 'description', 'creation_date', 'expires') readonly_fields = ('author', 'permissions', 'description', 'creation_date', 'expires', 'expired')
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False
@ -55,9 +55,9 @@ class AccessTokenInstanceInline(admin.TabularInline):
@admin.register(AccessToken) @admin.register(AccessToken)
class AccessTokenAdmin(admin.ModelAdmin): class AccessTokenAdmin(admin.ModelAdmin):
inlines = (AccessTokenInstanceInline,) inlines = (AccessTokenInstanceInline,)
list_display = ('__str__', 'user', 'permissions', 'author', 'creation_date', 'expires') list_display = ('__str__', 'user', 'permissions', 'author', 'creation_date', 'expires', 'expired')
fields = ('user', 'permissions', 'author', 'creation_date', 'expires') fields = ('user', 'permissions', 'author', 'creation_date', 'expires', 'expired')
readonly_fields = ('user', 'creation_date') readonly_fields = ('user', 'creation_date', 'expired')
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 01:43 # Generated by Django 1.10.4 on 2016-12-21 17:21
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
@ -20,8 +20,8 @@ class Migration(migrations.Migration):
name='AccessOperator', name='AccessOperator',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(verbose_name='description')), ('description', models.TextField(blank=True, null=True, verbose_name='description')),
('can_award_permissions', models.TextField(verbose_name='can award permissions')), ('can_award_permissions', models.CharField(max_length=2048, verbose_name='can award permissions')),
('access_from', models.DateTimeField(blank=True, null=True, verbose_name='has access from')), ('access_from', models.DateTimeField(blank=True, null=True, verbose_name='has access from')),
('access_until', models.DateTimeField(blank=True, null=True, verbose_name='has access until')), ('access_until', models.DateTimeField(blank=True, null=True, verbose_name='has access until')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='operator', to=settings.AUTH_USER_MODEL)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='operator', to=settings.AUTH_USER_MODEL)),
@ -35,10 +35,11 @@ class Migration(migrations.Migration):
name='AccessToken', name='AccessToken',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('permissions', models.TextField(verbose_name='permissions')), ('permissions', models.CharField(max_length=2048, verbose_name='permissions')),
('description', models.CharField(max_length=200, verbose_name='description')), ('description', models.CharField(max_length=200, verbose_name='description')),
('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
('expipres', models.DateTimeField(blank=True, null=True)), ('expires', models.DateTimeField(blank=True, null=True)),
('expired', models.BooleanField(default=False, verbose_name='is expired')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='creator')), ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='creator')),
], ],
options={ options={
@ -52,8 +53,8 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('secret', models.CharField(max_length=42, verbose_name='access secret')), ('secret', models.CharField(max_length=42, verbose_name='access secret')),
('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
('expipres', models.DateTimeField(null=True)), ('expires', models.DateTimeField(null=True)),
('access_token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='control.AccessToken', verbose_name='Access Token')), ('access_token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='access.AccessToken', verbose_name='Access Token')),
], ],
options={ options={
'verbose_name_plural': 'Access Tokens Instance', 'verbose_name_plural': 'Access Tokens Instance',
@ -64,10 +65,10 @@ class Migration(migrations.Migration):
name='AccessUser', name='AccessUser',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_url', models.CharField(help_text='Usually an URL to a profile somewhere', max_length=200, verbose_name='access name')), ('user_url', models.CharField(help_text='Usually an URL to a profile somewhere', max_length=200, unique=True, verbose_name='access name')),
('description', models.TextField(blank=True, max_length=200, null=True, verbose_name='description')), ('description', models.TextField(blank=True, max_length=200, null=True, verbose_name='description')),
('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='control.AccessOperator', verbose_name='creator')), ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='access.AccessOperator', verbose_name='creator')),
], ],
options={ options={
'verbose_name_plural': 'Access Users', 'verbose_name_plural': 'Access Users',
@ -77,6 +78,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='accesstoken', model_name='accesstoken',
name='user', name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='control.AccessUser', verbose_name='Access User'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='access.AccessUser', verbose_name='Access User'),
), ),
] ]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-21 17:39
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='accesstoken',
name='activated',
field=models.BooleanField(default=False, verbose_name='activated'),
),
migrations.AddField(
model_name='accesstoken',
name='secret',
field=models.CharField(default='', max_length=42, verbose_name='activation secret'),
preserve_default=False,
),
]

View file

@ -3,6 +3,8 @@ from datetime import timedelta
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Q
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -35,6 +37,14 @@ class AccessUser(models.Model):
verbose_name = _('Access User') verbose_name = _('Access User')
verbose_name_plural = _('Access Users') verbose_name_plural = _('Access Users')
@property
def valid_tokens(self):
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)
return self.tokens.create(**kwargs)
def __str__(self): def __str__(self):
return self.user_url return self.user_url
@ -47,11 +57,20 @@ class AccessToken(models.Model):
description = models.CharField(_('description'), max_length=200) description = models.CharField(_('description'), max_length=200)
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
expires = models.DateTimeField(null=True, blank=True) expires = models.DateTimeField(null=True, blank=True)
expired = models.BooleanField(_('is expired'), default=False)
activated = models.BooleanField(_('activated'), default=False)
secret = models.CharField(_('activation secret'), max_length=42)
class Meta: class Meta:
verbose_name = _('Access Token') verbose_name = _('Access Token')
verbose_name_plural = _('Access Tokens') verbose_name_plural = _('Access Tokens')
@property
def activation_url(self):
if self.activated:
return None
return reverse('access.activate', kwargs={'pk': self.pk, 'secret': self.secret})
def new_instance(self): def new_instance(self):
with transaction.atomic(): with transaction.atomic():
for instance in self.instances.filter(expires__isnull=True): for instance in self.instances.filter(expires__isnull=True):

View file

@ -0,0 +1,14 @@
{% extends 'access/base.html' %}
{% load bootstrap3 %}
{% load i18n %}
{% block bodyclass %}login{% endblock %}
{% block content %}
{% if success %}
<div class="alert alert-success">
<strong>{% trans 'Your access token was installed on your device!' %}</strong>
</div>
{% endif %}
{% endblock %}

View file

@ -9,14 +9,14 @@
<title>c3nav control panel</title> <title>c3nav control panel</title>
{% compress css %} {% compress css %}
<link href="{% static 'bootstrap/css/bootstrap.css' %}" rel="stylesheet"> <link href="{% static 'bootstrap/css/bootstrap.css' %}" rel="stylesheet">
<link href="{% static 'control/css/c3nav-control.css' %}" rel="stylesheet"> <link href="{% static 'access/css/c3nav-access.css' %}" rel="stylesheet">
{% endcompress %} {% endcompress %}
</head> </head>
<body class="{% block bodyclass %}{% endblock %}"> <body class="{% block bodyclass %}{% endblock %}">
<div class="container" id="main"> <div class="container" id="main">
<h1>c3nav control panel</h1> <h1>c3nav access control</h1>
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div> </div>

View file

@ -0,0 +1,3 @@
{% load i18n %}
<a href="{{ token.activation_url }}" class="btn btn-default btn-block">{% trans 'Activate token on this device' %}</a>

View file

@ -1,4 +1,4 @@
{% extends 'control/base.html' %} {% extends 'access/base.html' %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %} {% load i18n %}
@ -17,7 +17,7 @@
</fieldset> </fieldset>
<p>{% blocktrans %}This is the non-public backend for creation of auth tokens.{% endblocktrans %}</p> <p>{% blocktrans %}This is the non-public backend for creation of auth tokens.{% endblocktrans %}</p>
<p><a class="btn btn-sm btn-default btn-block" href="{% url 'control.prove' %}"> <p><a class="btn btn-sm btn-default btn-block" href="{% url 'access.prove' %}">
{% blocktrans %}Prove that you should have access{% endblocktrans %} {% blocktrans %}Prove that you should have access{% endblocktrans %}
</a></p> </a></p>
</form> </form>

View file

@ -1,4 +1,4 @@
{% extends 'control/base.html' %} {% extends 'access/base.html' %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %} {% load i18n %}
@ -14,8 +14,10 @@
{% if success %} {% if success %}
<div class="alert alert-success"> <div class="alert alert-success">
<strong>{% trans 'Thanks you get full access to the map!' %}</strong><br> <strong>{% trans 'Thanks you get full access to the map!' %}</strong><br>
{{ token }} {% if replaced %}{% trans 'All previous tokens have been invalidated.' %}<br>{% endif %}
</div> </div>
{% include 'access/fragment_token.html' with token=token %}
{% elif hosters %} {% elif hosters %}
{% if error %} {% if error %}
<div class="alert alert-dismissible alert-danger"> <div class="alert alert-dismissible alert-danger">
@ -23,7 +25,7 @@
{% if error == 'invalid' %} {% if error == 'invalid' %}
<strong>{% trans 'Sorry.' %}</strong> {% trans 'One or more access tokens were not correct.' %} <strong>{% trans 'Sorry.' %}</strong> {% trans 'One or more access tokens were not correct.' %}
{% elif error == 'duplicate' %} {% elif error == 'duplicate' %}
<strong>{% trans 'Sorry.' %}</strong> {% trans 'You already have an access token.' %} <strong>{% trans 'Sorry.' %}</strong> {% trans 'You already have a valid access token.' %}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -33,6 +35,11 @@
<input type="password" class="form-control" id="hoster{{ forloop.counter0 }}" name="{{ package.name }}" placeholder="{% trans 'Access Token' %}"> <input type="password" class="form-control" id="hoster{{ forloop.counter0 }}" name="{{ package.name }}" placeholder="{% trans 'Access Token' %}">
</div> </div>
{% endfor %} {% endfor %}
<div class="checkbox">
<label>
<input type="checkbox" name="replace" value="1"> {% trans 'Invalidate previus token(s).' %}
</label>
</div>
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary btn-block btn-lg">{% trans 'Submit' %}</button> <button type="submit" class="btn btn-primary btn-block btn-lg">{% trans 'Submit' %}</button>
</div> </div>

12
src/c3nav/access/urls.py Normal file
View file

@ -0,0 +1,12 @@
from django.conf.urls import url
from django.contrib.auth import views as auth_views
from c3nav.access.views import activate_token, dashboard, prove
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'^login/$', auth_views.login, {'template_name': 'access/login.html'}, name='access.login'),
url(r'^logout/$', auth_views.logout, name='access.logout'),
]

76
src/c3nav/access/views.py Normal file
View file

@ -0,0 +1,76 @@
from collections import OrderedDict
from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.shortcuts import get_object_or_404, render
from c3nav.access.models import AccessToken, AccessUser
from c3nav.editor.hosters import get_hoster_for_package
from c3nav.mapdata.permissions import get_nonpublic_packages
@login_required(login_url='/access/login/')
def dashboard(request):
return render(request, 'access/dashboard.html')
def prove(request):
hosters = OrderedDict((package, get_hoster_for_package(package)) for package in get_nonpublic_packages())
if not hosters or None in hosters.values():
return render(request, 'access/prove.html', context={'hosters': None})
error = None
if request.method == 'POST':
user_id = None
for package, hoster in hosters.items():
access_token = request.POST.get(package.name)
hoster_user_id = hoster.get_user_id_with_access_token(access_token)
if hoster_user_id is None:
return render(request, 'access/prove.html', context={
'hosters': hosters,
'error': 'invalid',
})
if user_id is None:
user_id = hoster_user_id
replaced = False
with transaction.atomic():
user = AccessUser.objects.filter(user_url=user_id).first()
if user is not None:
valid_tokens = user.valid_tokens
if valid_tokens.count():
if request.POST.get('replace') != '1':
return render(request, 'access/prove.html', context={
'hosters': hosters,
'error': 'duplicate',
})
for token in valid_tokens:
token.expired = True
token.save()
replaced = True
else:
user = AccessUser.objects.create(user_url=user_id)
token = user.new_token(permissions=':all', description='automatically created')
return render(request, 'access/prove.html', context={
'hosters': hosters,
'success': True,
'replaced': replaced,
'token': token,
})
return render(request, 'access/prove.html', context={
'hosters': hosters,
'error': error,
})
def activate_token(request, pk, secret):
token = get_object_or_404(AccessToken, expired=False, id=pk, secret=secret) # noqa
return render(request, 'access/activate.html', context={
'success': True,
})

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 01:58
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('control', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='accessoperator',
name='description',
field=models.TextField(blank=True, null=True, verbose_name='description'),
),
]

View file

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 02:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('control', '0002_auto_20161220_0158'),
]
operations = [
migrations.RenameField(
model_name='accesstoken',
old_name='expipres',
new_name='expires',
),
migrations.RenameField(
model_name='accesstokeninstance',
old_name='expipres',
new_name='expires',
),
migrations.AlterField(
model_name='accessuser',
name='user_url',
field=models.CharField(help_text='Usually an URL to a profile somewhere', max_length=200, unique=True, verbose_name='access name'),
),
]

View file

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 02:53
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('control', '0003_auto_20161220_0214'),
]
operations = [
migrations.AlterField(
model_name='accessoperator',
name='can_award_permissions',
field=models.CharField(max_length=2048, verbose_name='can award permissions'),
),
migrations.AlterField(
model_name='accesstoken',
name='permissions',
field=models.CharField(max_length=2048, verbose_name='permissions'),
),
]

View file

@ -1,11 +0,0 @@
from django.conf.urls import url
from django.contrib.auth import views as auth_views
from c3nav.control.views import dashboard, prove
urlpatterns = [
url(r'^$', dashboard, name='control.dashboard'),
url(r'^prove/$', prove, name='control.prove'),
url(r'^login/$', auth_views.login, {'template_name': 'control/login.html'}, name='site.login'),
url(r'^logout/$', auth_views.logout, name='site.logout'),
]

View file

@ -1,58 +0,0 @@
from collections import OrderedDict
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from c3nav.control.models import AccessUser
from c3nav.editor.hosters import get_hoster_for_package
from c3nav.mapdata.permissions import get_nonpublic_packages
@login_required(login_url='/control/login/')
def dashboard(request):
return render(request, 'control/dashboard.html')
def prove(request):
hosters = OrderedDict((package, get_hoster_for_package(package)) for package in get_nonpublic_packages())
if not hosters or None in hosters.values():
return render(request, 'control/prove.html', context={'hosters': None})
error = None
if request.method == 'POST':
user_id = None
for package, hoster in hosters.items():
access_token = request.POST.get(package.name)
hoster_user_id = hoster.get_user_id_with_access_token(access_token)
if hoster_user_id is None:
return render(request, 'control/prove.html', context={
'hosters': hosters,
'error': 'invalid',
})
if user_id is None:
user_id = hoster_user_id
user = AccessUser.objects.filter(user_url=user_id).first()
if user is not None:
if user.tokens.count():
return render(request, 'control/prove.html', context={
'hosters': hosters,
'error': 'duplicate',
})
else:
user = AccessUser.objects.create(user_url=user_id)
token = user.tokens.create(permissions=':all', description='automatically created')
token_instance = token.new_instance()
return render(request, 'control/prove.html', context={
'hosters': hosters,
'success': True,
'token': token_instance,
})
return render(request, 'control/prove.html', context={
'hosters': hosters,
'error': error,
})

View file

@ -145,10 +145,10 @@ INSTALLED_APPS = [
'c3nav.api', 'c3nav.api',
'rest_framework', 'rest_framework',
'c3nav.mapdata', 'c3nav.mapdata',
'c3nav.access',
'c3nav.routing', 'c3nav.routing',
'c3nav.site', 'c3nav.site',
'c3nav.editor', 'c3nav.editor',
'c3nav.control',
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE_CLASSES = [

View file

@ -1,13 +1,13 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
import c3nav.access.urls
import c3nav.api.urls import c3nav.api.urls
import c3nav.control.urls
import c3nav.editor.urls import c3nav.editor.urls
import c3nav.site.urls import c3nav.site.urls
urlpatterns = [ urlpatterns = [
url(r'^control/', include(c3nav.control.urls)), url(r'^access/', include(c3nav.access.urls)),
url(r'^editor/', include(c3nav.editor.urls)), url(r'^editor/', include(c3nav.editor.urls)),
url(r'^api/', include(c3nav.api.urls, namespace='api')), url(r'^api/', include(c3nav.api.urls, namespace='api')),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),