From edb6ee61cfdf7c57eb523a7f6827749b2be174c3 Mon Sep 17 00:00:00 2001
From: Enrico Zini
Date: Mon, 16 May 2016 17:24:27 +0200
Subject: [PATCH] Added prototype dm claim account interface
---
backend/models.py | 4 +
dm/__init__.py | 0
dm/admin.py | 3 +
dm/migrations/__init__.py | 0
dm/models.py | 1 +
dm/templates/dm/claim.html | 44 ++++++++++
dm/templates/dm/claim_confirm.html | 25 ++++++
dm/tests.py | 3 +
dm/urls.py | 15 ++++
dm/views.py | 136 +++++++++++++++++++++++++++++
nm2/settings.py | 1 +
nmlayout/templates/nm-base.html | 2 +-
urls.py | 23 ++---
13 files changed, 239 insertions(+), 18 deletions(-)
create mode 100644 dm/__init__.py
create mode 100644 dm/admin.py
create mode 100644 dm/migrations/__init__.py
create mode 100644 dm/models.py
create mode 100644 dm/templates/dm/claim.html
create mode 100644 dm/templates/dm/claim_confirm.html
create mode 100644 dm/tests.py
create mode 100644 dm/urls.py
create mode 100644 dm/views.py
diff --git a/backend/models.py b/backend/models.py
index f213c1e4..6d7d6532 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -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.
diff --git a/dm/__init__.py b/dm/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/dm/admin.py b/dm/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/dm/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/dm/migrations/__init__.py b/dm/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/dm/models.py b/dm/models.py
new file mode 100644
index 00000000..137941ff
--- /dev/null
+++ b/dm/models.py
@@ -0,0 +1 @@
+from django.db import models
diff --git a/dm/templates/dm/claim.html b/dm/templates/dm/claim.html
new file mode 100644
index 00000000..f32b020a
--- /dev/null
+++ b/dm/templates/dm/claim.html
@@ -0,0 +1,44 @@
+{% extends "public/base.html" %}
+{% load nm %}
+
+{% block content %}
+
+Associate SSO username to existing person
+
+For many Debian Maintainer entries, the site does not currently know the
+corresponding username that is used by the Debian Single Sign-On,
+and therefore cannot give you permission on your own data. This page helps to
+fix this situation.
+
+
+Since you are logged in, I already know that your Single Sign-On username is
+{{username}}. 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:
+
+
+
+{% if person %}
+
+
+Ok, so you claim to be {{person}}, is
+that right? If that is not correct, please change the fingerprint in the form
+above and click "Update".
+
+
+
+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 {{person}}.
+
+
+
+{{challenge}}
+
+
+{% endif %}
+
+{% endblock %}
diff --git a/dm/templates/dm/claim_confirm.html b/dm/templates/dm/claim_confirm.html
new file mode 100644
index 00000000..75451a71
--- /dev/null
+++ b/dm/templates/dm/claim_confirm.html
@@ -0,0 +1,25 @@
+{% extends "public/base.html" %}
+{% load nm %}
+
+{% block content %}
+
+Confirm association of SSO username to existing person
+
+{% if errors %}
+There have been error with your confirmation email:
+
+ {% for e in errors %}
+ - {{e}}
+ {% endfor %}
+
+
+{% endif %}
+
+{% if mapped %}
+Your Single Sing-On account name {{username}} is now associated to
+{{person}}. If you visit that page,
+you should now be recognised as the owner of the page.
+{% endif %}
+
+{% endblock %}
+
diff --git a/dm/tests.py b/dm/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/dm/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/dm/urls.py b/dm/urls.py
new file mode 100644
index 00000000..253bc780
--- /dev/null
+++ b/dm/urls.py
@@ -0,0 +1,15 @@
+# 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[^/]+)$', views.ClaimConfirm.as_view(), name="dm_claim_confirm"),
+]
+
diff --git a/dm/views.py b/dm/views.py
new file mode 100644
index 00000000..895c51a8
--- /dev/null
+++ b/dm/views.py
@@ -0,0 +1,136 @@
+# 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
diff --git a/nm2/settings.py b/nm2/settings.py
index ea804888..c845b097 100644
--- a/nm2/settings.py
+++ b/nm2/settings.py
@@ -146,6 +146,7 @@ INSTALLED_APPS = [
'apikeys',
'public',
'restricted',
+ 'dm',
'maintenance',
'projectb',
'minechangelogs',
diff --git a/nmlayout/templates/nm-base.html b/nmlayout/templates/nm-base.html
index 9caf4095..1ad5d451 100644
--- a/nmlayout/templates/nm-base.html
+++ b/nmlayout/templates/nm-base.html
@@ -33,7 +33,7 @@
{% block relatedpages %}
{% if user.is_anonymous %}
{% if request.sso_username %}
- {{request.sso_username}} not known to this site yet
+ {{request.sso_username}} not known to this site yet
{% else %}
login
{% endif %}
diff --git a/urls.py b/urls.py
index 8c4d35b0..5485affa 100644
--- a/urls.py
+++ b/urls.py
@@ -1,20 +1,8 @@
-# NM website
-#
-# Copyright (C) 2012--2013 Enrico Zini
-#
-# 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 .
-
+# 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")),
--
GitLab