From adaf7328093bbc56d48c9732344767838e008780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Tue, 20 Dec 2016 03:53:28 +0100 Subject: [PATCH] first stuff for access tokens --- src/c3nav/control/admin.py | 64 ++++++++++++++- src/c3nav/control/migrations/0001_initial.py | 82 +++++++++++++++++++ .../migrations/0002_auto_20161220_0158.py | 20 +++++ .../migrations/0003_auto_20161220_0214.py | 30 +++++++ .../migrations/0004_auto_20161220_0253.py | 25 ++++++ src/c3nav/control/models.py | 81 +++++++++++++++++- .../control/templates/control/prove.html | 9 +- src/c3nav/control/views.py | 42 +++++++--- src/c3nav/editor/hosters/gitlab.py | 2 +- 9 files changed, 336 insertions(+), 19 deletions(-) create mode 100644 src/c3nav/control/migrations/0001_initial.py create mode 100644 src/c3nav/control/migrations/0002_auto_20161220_0158.py create mode 100644 src/c3nav/control/migrations/0003_auto_20161220_0214.py create mode 100644 src/c3nav/control/migrations/0004_auto_20161220_0253.py diff --git a/src/c3nav/control/admin.py b/src/c3nav/control/admin.py index 6af52da2..b339dbba 100644 --- a/src/c3nav/control/admin.py +++ b/src/c3nav/control/admin.py @@ -1,3 +1,63 @@ -from django.contrib import admin # noqa +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import User +from django.utils.translation import ugettext_lazy as _ -# Register your models here. +from c3nav.control.models import AccessOperator, AccessToken, AccessTokenInstance, AccessUser + + +class AccessOperatorInline(admin.StackedInline): + model = AccessOperator + can_delete = False + + +class UserAdmin(BaseUserAdmin): + fieldsets = ( + (None, {'fields': ('username', 'password', 'email')}), + (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', + 'groups', 'user_permissions')}), + (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + ) + readonly_fields = ('last_login', 'date_joined',) + inlines = (AccessOperatorInline, ) + + +admin.site.unregister(User) +admin.site.register(User, UserAdmin) + + +class AccessTokenInline(admin.TabularInline): + model = AccessToken + show_change_link = True + readonly_fields = ('author', 'permissions', 'description', 'creation_date', 'expires') + + def has_add_permission(self, request): + return False + + +@admin.register(AccessUser) +class AccessUserAdmin(admin.ModelAdmin): + inlines = (AccessTokenInline,) + list_display = ('user_url', 'creation_date', 'author', 'description') + fields = ('user_url', 'creation_date', 'author', 'description') + readonly_fields = ('creation_date', ) + + +class AccessTokenInstanceInline(admin.TabularInline): + model = AccessTokenInstance + fields = ('secret', 'creation_date', 'expires', ) + readonly_fields = ('secret', 'creation_date', 'expires', ) + + def has_add_permission(self, request): + return False + + +@admin.register(AccessToken) +class AccessTokenAdmin(admin.ModelAdmin): + inlines = (AccessTokenInstanceInline,) + list_display = ('__str__', 'user', 'permissions', 'author', 'creation_date', 'expires') + fields = ('user', 'permissions', 'author', 'creation_date', 'expires') + readonly_fields = ('user', 'creation_date') + + def has_add_permission(self, request): + return False diff --git a/src/c3nav/control/migrations/0001_initial.py b/src/c3nav/control/migrations/0001_initial.py new file mode 100644 index 00000000..4d97abf9 --- /dev/null +++ b/src/c3nav/control/migrations/0001_initial.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-20 01:43 +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): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AccessOperator', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(verbose_name='description')), + ('can_award_permissions', models.TextField(verbose_name='can award permissions')), + ('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')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='operator', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Access Operator', + 'verbose_name': 'Access Operator', + }, + ), + migrations.CreateModel( + name='AccessToken', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('permissions', models.TextField(verbose_name='permissions')), + ('description', models.CharField(max_length=200, verbose_name='description')), + ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), + ('expipres', models.DateTimeField(blank=True, null=True)), + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='creator')), + ], + options={ + 'verbose_name_plural': 'Access Tokens', + 'verbose_name': 'Access Token', + }, + ), + migrations.CreateModel( + name='AccessTokenInstance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('secret', models.CharField(max_length=42, verbose_name='access secret')), + ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), + ('expipres', 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')), + ], + options={ + 'verbose_name_plural': 'Access Tokens Instance', + 'verbose_name': 'Access Token Instance', + }, + ), + migrations.CreateModel( + name='AccessUser', + fields=[ + ('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')), + ('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')), + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='control.AccessOperator', verbose_name='creator')), + ], + options={ + 'verbose_name_plural': 'Access Users', + 'verbose_name': 'Access User', + }, + ), + migrations.AddField( + model_name='accesstoken', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='control.AccessUser', verbose_name='Access User'), + ), + ] diff --git a/src/c3nav/control/migrations/0002_auto_20161220_0158.py b/src/c3nav/control/migrations/0002_auto_20161220_0158.py new file mode 100644 index 00000000..51376a6e --- /dev/null +++ b/src/c3nav/control/migrations/0002_auto_20161220_0158.py @@ -0,0 +1,20 @@ +# -*- 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'), + ), + ] diff --git a/src/c3nav/control/migrations/0003_auto_20161220_0214.py b/src/c3nav/control/migrations/0003_auto_20161220_0214.py new file mode 100644 index 00000000..fc7d7773 --- /dev/null +++ b/src/c3nav/control/migrations/0003_auto_20161220_0214.py @@ -0,0 +1,30 @@ +# -*- 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'), + ), + ] diff --git a/src/c3nav/control/migrations/0004_auto_20161220_0253.py b/src/c3nav/control/migrations/0004_auto_20161220_0253.py new file mode 100644 index 00000000..7be20c2d --- /dev/null +++ b/src/c3nav/control/migrations/0004_auto_20161220_0253.py @@ -0,0 +1,25 @@ +# -*- 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'), + ), + ] diff --git a/src/c3nav/control/models.py b/src/c3nav/control/models.py index 9d57c559..859a4677 100644 --- a/src/c3nav/control/models.py +++ b/src/c3nav/control/models.py @@ -1,3 +1,80 @@ -from django.db import models # noqa +import string +from datetime import timedelta -# Create your models here. +from django.contrib.auth.models import User +from django.db import models, transaction +from django.utils import timezone +from django.utils.crypto import get_random_string +from django.utils.translation import ugettext_lazy as _ + + +class AccessOperator(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='operator') + description = models.TextField(_('description'), null=True, 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) + + class Meta: + verbose_name = _('Access Operator') + verbose_name_plural = _('Access Operator') + + def __str__(self): + return str(self.user) + + +class AccessUser(models.Model): + user_url = models.CharField(_('access name'), unique=True, max_length=200, + 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) + creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) + + class Meta: + verbose_name = _('Access User') + verbose_name_plural = _('Access Users') + + def __str__(self): + return self.user_url + + +class AccessToken(models.Model): + user = models.ForeignKey(AccessUser, on_delete=models.CASCADE, related_name='tokens', + verbose_name=_('Access User')) + author = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name=_('creator'), null=True, blank=True) + permissions = models.CharField(_('permissions'), max_length=2048) + description = models.CharField(_('description'), max_length=200) + creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) + expires = models.DateTimeField(null=True, blank=True) + + class Meta: + verbose_name = _('Access Token') + verbose_name_plural = _('Access Tokens') + + def new_instance(self): + with transaction.atomic(): + for instance in self.instances.filter(expires__isnull=True): + instance.expires = timezone.now()+timedelta(seconds=5) + instance.save() + + self.instances.filter(expires__isnull=False, expires__lt=timezone.now()).delete() + + secret = get_random_string(42, string.ascii_letters+string.digits) + self.instances.create(secret=secret) + return '%d:%s' % (self.pk, secret) + + def __str__(self): + return '%s #%d' % (_('Access Token'), self.id) + + +class AccessTokenInstance(models.Model): + access_token = models.ForeignKey(AccessToken, on_delete=models.CASCADE, related_name='instances', + verbose_name=_('Access Token')) + secret = models.CharField(_('access secret'), max_length=42) + creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) + expires = models.DateTimeField(null=True) + + class Meta: + verbose_name = _('Access Token Instance') + verbose_name_plural = _('Access Tokens Instance') diff --git a/src/c3nav/control/templates/control/prove.html b/src/c3nav/control/templates/control/prove.html index 4cf685b7..213541c4 100644 --- a/src/c3nav/control/templates/control/prove.html +++ b/src/c3nav/control/templates/control/prove.html @@ -13,13 +13,18 @@

{% blocktrans %}Please enter a valid authentication code for the hosters of the following non-public map packages:{% endblocktrans %}

{% if success %}
- {% trans 'Thanks – you get full access to the map!' %} + {% trans 'Thanks – you get full access to the map!' %}
+ {{ token }}
{% elif hosters %} {% if error %}
- {% trans 'Sorry.' %} {% trans 'One or more access tokens were not correct.' %} + {% if error == 'invalid' %} + {% trans 'Sorry.' %} {% trans 'One or more access tokens were not correct.' %} + {% elif error == 'duplicate' %} + {% trans 'Sorry.' %} {% trans 'You already have an access token.' %} + {% endif %}
{% endif %} {% for package in hosters %} diff --git a/src/c3nav/control/views.py b/src/c3nav/control/views.py index e2560691..c233e468 100644 --- a/src/c3nav/control/views.py +++ b/src/c3nav/control/views.py @@ -3,6 +3,7 @@ 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 @@ -15,26 +16,43 @@ def dashboard(request): def prove(request): hosters = OrderedDict((package, get_hoster_for_package(package)) for package in get_nonpublic_packages()) - if None in hosters.values(): + if not hosters or None in hosters.values(): return render(request, 'control/prove.html', context={'hosters': None}) - error = False - success = False + error = None if request.method == 'POST': - user_ids = {} + user_id = None for package, hoster in hosters.items(): access_token = request.POST.get(package.name) - user_id = hoster.get_user_id_with_access_token(access_token) - if user_id is None: - error = True - break - user_ids[hoster] = user_id + 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 not error: - success = True + 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, - 'success': success, }) diff --git a/src/c3nav/editor/hosters/gitlab.py b/src/c3nav/editor/hosters/gitlab.py index b4e4cc52..2eae0b34 100644 --- a/src/c3nav/editor/hosters/gitlab.py +++ b/src/c3nav/editor/hosters/gitlab.py @@ -147,4 +147,4 @@ class GitlabHoster(Hoster): response = requests.get(self.base_url + 'api/v3/user?private_token=' + access_token) if response.status_code != 200: return None - return str(response.json()['id']) + return self.base_url+'user/'+str(response.json()['id'])