Commit edb6ee61 authored by Enrico Zini's avatar Enrico Zini
Browse files

Added prototype dm claim account interface

parent 79ef5189
......@@ -790,6 +790,10 @@ class Fingerprint(models.Model):
fpr = FingerprintField(verbose_name="OpenPGP key fingerprint", max_length=40, unique=True)
is_active = models.BooleanField(default=False, help_text="whether this key is curently in use")
def get_key(self):
from keyring.models import Key
return Key.objects.get_or_download(self.fpr)
def save(self, *args, **kw):
"""
Save, and add an entry to the Person audit log.
......
from django.contrib import admin
# Register your models here.
from django.db import models
{% extends "public/base.html" %}
{% load nm %}
{% block content %}
<h1>Associate SSO username to existing person</h1>
<p>For many Debian Maintainer entries, the site does not currently know the
corresponding username that is used by the <a href="https://wiki.debian.org/DebianSingleSignOn">Debian Single Sign-On</a>,
and therefore cannot give you permission on your own data. This page helps to
fix this situation.</p>
<p>Since you are logged in, I already know that your Single Sign-On username is
<b>{{username}}</b>. The missing bit of information is who are you in the site,
and I would like to find out using your GPG key. Please enter the fingerprint
in this form:</p>
<form action="" method="POST">{% csrf_token %}
{{form.as_p}}
<button type="submit">Update</button>
</form>
{% if person %}
<p>
Ok, so you claim to be <a href="{{person.get_absolute_url}}">{{person}}</a>, is
that right? If that is not correct, please change the fingerprint in the form
above and click "Update".
</p>
<p>
If it is correct, I need a way to trust you. Please decrypt the GPG snippet
that you find after this paragraph, and you will find a URL. Visit that URL and
I will be able to trust that you are indeed <a href="{{person.get_absolute_url}}">{{person}}</a>.
</p>
<pre>
{{challenge}}
</pre>
{% endif %}
{% endblock %}
{% extends "public/base.html" %}
{% load nm %}
{% block content %}
<h1>Confirm association of SSO username to existing person</h1>
{% if errors %}
<p>There have been error with your confirmation email:
<ul class="condensed">
{% for e in errors %}
<li>{{e}}</li>
{% endfor %}
</ul>
</p>
{% endif %}
{% if mapped %}
<p>Your Single Sing-On account name <b>{{username}}</b> is now associated to
<a href="{{person.get_absolute_url}}">{{person}}</a>. If you visit that page,
you should now be recognised as the owner of the page.</p>
{% endif %}
{% endblock %}
from django.test import TestCase
# Create your tests here.
# coding: utf-8
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from django.conf.urls import url
from django.views.generic import RedirectView
from . import views
urlpatterns = [
url(r'^$', RedirectView.as_view(url="/", permanent=True), name="dm_index"),
url(r'^claim$', views.Claim.as_view(), name="dm_claim"),
url(r'^claim/confirm/(?P<token>[^/]+)$', views.ClaimConfirm.as_view(), name="dm_claim_confirm"),
]
# coding: utf8
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from django.shortcuts import render
from django.views.generic import TemplateView, View
from django.views.generic.edit import FormView
from django.core import signing
from django.core.urlresolvers import reverse
from django import forms
from backend.mixins import VisitorMixin
import backend.models as bmodels
def is_valid_username(username):
if username.endswith("@users.alioth.debian.org"): return True
if username.endswith("@debian.org"): return True
return False
class ClaimForm(forms.Form):
fpr = forms.CharField(label="Fingerprint", min_length=40, widget=forms.TextInput(attrs={"size": 60}))
def clean_fpr(self):
data = bmodels.FingerprintField.clean_fingerprint(self.cleaned_data['fpr'])
try:
fpr = bmodels.Fingerprint.objects.get(fpr=self.cleaned_data["fpr"])
except bmodels.Fingerprint.DoesNotExist:
raise forms.ValidationError("The GPG fingerprint is not known to this system. "
"If you are a Debian Maintainer, and you entered the fingerprint that is in the DM keyring, "
"please contact Front Desk to get this fixed.")
if not fpr.is_active:
raise forms.ValidationError("The GPG fingerprint corresponds to a key that is not currently the active key of the user.")
if is_valid_username(fpr.user.username):
raise forms.ValidationError("The GPG fingerprint corresponds to a person that has a valid Single Sign-On username.")
return data
class Claim(VisitorMixin, FormView):
"""
Validate and send an encrypted HMAC url to associate an alioth account with
a DM key
"""
template_name = "dm/claim.html"
form_class = ClaimForm
def pre_dispatch(self):
super(Claim, self).pre_dispatch()
if self.visitor is not None: raise PermissionDenied
if self.request.sso_username is None: raise PermissionDenied
if not is_valid_username(self.request.sso_username): raise PermissionDenied
self.username = self.request.sso_username
def get_context_data(self, fpr=None, **kw):
ctx = super(Claim, self).get_context_data(**kw)
ctx["username"] = self.username
if fpr:
ctx["fpr"] = fpr
ctx["person"] = fpr.user
key = fpr.get_key()
if not key.key_is_fresh(): key.update_key()
plaintext = self.request.build_absolute_uri(reverse("dm_claim_confirm", kwargs={
"token": signing.dumps({
"u": self.username,
"f": fpr.fpr,
})
}))
plaintext += "\n"
ctx["challenge"] = key.encrypt(plaintext.encode("utf8"))
return ctx
def form_valid(self, form):
fpr = bmodels.Fingerprint.objects.get(fpr=form.cleaned_data["fpr"])
return self.render_to_response(self.get_context_data(form=form, fpr=fpr))
class ClaimConfirm(VisitorMixin, TemplateView):
"""
Validate the claim confirmation links
"""
template_name = "dm/claim_confirm.html"
def validate_token(self, token):
parsed = signing.loads(token)
self.errors = []
if self.visitor is not None:
self.errors.append("Your SSO username is already associated with a person in the system")
return False
# Validate fingerprint
try:
self.fpr = bmodels.Fingerprint.objects.get(fpr=parsed["f"])
except bmodels.Fingerprint.DoesNotExist:
self.fpr = None
self.errors.append("The GPG fingerprint is not known to this system")
return False
if not self.fpr.is_active:
self.errors.append("The GPG fingerprint corresponds to a key that is not currently the active key of the user.")
if is_valid_username(self.fpr.user.username):
self.errors.append("The GPG fingerprint corresponds to a person that has a valid Single Sign-On username.")
# Validate username
self.username = parsed["u"]
if not is_valid_username(self.username):
self.errors.append("The username does not look like a valid SSO username")
try:
existing_person = bmodels.Person.objects.get(username=self.username)
except bmodels.Person.DoesNotExist:
existing_person = None
if existing_person is not None:
self.errors.append("The SSO username is already associated with a different person in the system")
return not self.errors
def get_context_data(self, **kw):
ctx = super(ClaimConfirm, self).get_context_data(**kw)
if self.validate_token(self.kwargs["token"]):
# Do the mapping
self.fpr.person.username = self.username
self.fpr.person.save()
ctx["mapped"] = True
ctx["person"] = self.fpr.person
ctx["fpr"] = self.fpr
ctx["username"] = self.username
ctx["errors"] = self.errors
return ctx
......@@ -146,6 +146,7 @@ INSTALLED_APPS = [
'apikeys',
'public',
'restricted',
'dm',
'maintenance',
'projectb',
'minechangelogs',
......
......@@ -33,7 +33,7 @@
{% block relatedpages %}
{% if user.is_anonymous %}
{% if request.sso_username %}
<a href="{% url 'public_newnm' %}">{{request.sso_username}} not known to this site yet</a>
<a href="{% url 'dm_claim' %}">{{request.sso_username}} not known to this site yet</a>
{% else %}
<a href="https://sso.debian.org/sso/login?url={{request.build_absolute_uri}}&site=NM">login</a>
{% endif %}
......
# NM website
#
# Copyright (C) 2012--2013 Enrico Zini <enrico@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# coding: utf-8
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from django.conf.urls import patterns, include, url
from django.views.generic import TemplateView
from django_dacs import views as django_dacs_views
......@@ -33,6 +21,7 @@ urlpatterns = [
# DACS login
url(r'^public/', include("public.urls")),
url(r'^am/', include("restricted.urls")),
url(r'^dm/', include("dm.urls")),
url(r'^api/', include("api.urls")),
url(r'^apikeys/', include("apikeys.urls")),
url(r'^keyring/', include("keyring.urls")),
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment