first stuff for access tokens
This commit is contained in:
parent
b26a3548fc
commit
adaf732809
9 changed files with 336 additions and 19 deletions
|
@ -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
|
||||||
|
|
82
src/c3nav/control/migrations/0001_initial.py
Normal file
82
src/c3nav/control/migrations/0001_initial.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
20
src/c3nav/control/migrations/0002_auto_20161220_0158.py
Normal file
20
src/c3nav/control/migrations/0002_auto_20161220_0158.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
30
src/c3nav/control/migrations/0003_auto_20161220_0214.py
Normal file
30
src/c3nav/control/migrations/0003_auto_20161220_0214.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
25
src/c3nav/control/migrations/0004_auto_20161220_0253.py
Normal file
25
src/c3nav/control/migrations/0004_auto_20161220_0253.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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')
|
||||||
|
|
|
@ -13,13 +13,18 @@
|
||||||
<p>{% blocktrans %}Please enter a valid authentication code for the hosters of the following non-public map packages:{% endblocktrans %}</p>
|
<p>{% blocktrans %}Please enter a valid authentication code for the hosters of the following non-public map packages:{% endblocktrans %}</p>
|
||||||
{% 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>
|
<strong>{% trans 'Thanks – you get full access to the map!' %}</strong><br>
|
||||||
|
{{ token }}
|
||||||
</div>
|
</div>
|
||||||
{% elif hosters %}
|
{% elif hosters %}
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-dismissible alert-danger">
|
<div class="alert alert-dismissible alert-danger">
|
||||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
{% 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' %}
|
||||||
|
<strong>{% trans 'Sorry.' %}</strong> {% trans 'You already have an access token.' %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for package in hosters %}
|
{% for package in hosters %}
|
||||||
|
|
|
@ -3,6 +3,7 @@ from collections import OrderedDict
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from c3nav.control.models import AccessUser
|
||||||
from c3nav.editor.hosters import get_hoster_for_package
|
from c3nav.editor.hosters import get_hoster_for_package
|
||||||
from c3nav.mapdata.permissions import get_nonpublic_packages
|
from c3nav.mapdata.permissions import get_nonpublic_packages
|
||||||
|
|
||||||
|
@ -15,26 +16,43 @@ def dashboard(request):
|
||||||
def prove(request):
|
def prove(request):
|
||||||
hosters = OrderedDict((package, get_hoster_for_package(package)) for package in get_nonpublic_packages())
|
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})
|
return render(request, 'control/prove.html', context={'hosters': None})
|
||||||
|
|
||||||
error = False
|
error = None
|
||||||
success = False
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
user_ids = {}
|
user_id = None
|
||||||
for package, hoster in hosters.items():
|
for package, hoster in hosters.items():
|
||||||
access_token = request.POST.get(package.name)
|
access_token = request.POST.get(package.name)
|
||||||
user_id = hoster.get_user_id_with_access_token(access_token)
|
hoster_user_id = hoster.get_user_id_with_access_token(access_token)
|
||||||
if user_id is None:
|
if hoster_user_id is None:
|
||||||
error = True
|
return render(request, 'control/prove.html', context={
|
||||||
break
|
'hosters': hosters,
|
||||||
user_ids[hoster] = user_id
|
'error': 'invalid',
|
||||||
|
})
|
||||||
|
|
||||||
if not error:
|
if user_id is None:
|
||||||
success = True
|
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={
|
return render(request, 'control/prove.html', context={
|
||||||
'hosters': hosters,
|
'hosters': hosters,
|
||||||
'error': error,
|
'error': error,
|
||||||
'success': success,
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -147,4 +147,4 @@ class GitlabHoster(Hoster):
|
||||||
response = requests.get(self.base_url + 'api/v3/user?private_token=' + access_token)
|
response = requests.get(self.base_url + 'api/v3/user?private_token=' + access_token)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return None
|
return None
|
||||||
return str(response.json()['id'])
|
return self.base_url+'user/'+str(response.json()['id'])
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue