summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDave Page2016-03-15 11:31:53 +0000
committerDave Page2016-03-15 11:31:53 +0000
commite52643477ef45cfe03a44da9f675b77da5656ff3 (patch)
tree0a1e1a9a7ec20912ec150b1cc6343de66f7f67be
parentee46393b077a969361e4201bc9088652500f8b4b (diff)
Add PG Community Auth support.
-rw-r--r--web/pgperffarm/auth.py205
-rw-r--r--web/pgperffarm/settings.py4
-rw-r--r--web/pgperffarm/settings_local.py.in2
-rw-r--r--web/pgperffarm/urls.py7
-rw-r--r--web/pgperffarm/views.py12
-rw-r--r--web/templates/admin/login.html11
-rw-r--r--web/templates/body.html11
7 files changed, 242 insertions, 10 deletions
diff --git a/web/pgperffarm/auth.py b/web/pgperffarm/auth.py
new file mode 100644
index 0000000..5ce4593
--- /dev/null
+++ b/web/pgperffarm/auth.py
@@ -0,0 +1,205 @@
+#
+# Django module to support postgresql.org community authentication 2.0
+#
+# The main location for this module is the pgweb git repository hosted
+# on git.postgresql.org - look there for updates.
+#
+# To integrate with django, you need the following:
+# * Make sure the view "login" from this module is used for login
+# * Map an url somwehere (typically /auth_receive/) to the auth_receive
+# view.
+# * In settings.py, set AUTHENTICATION_BACKENDS to point to the class
+# AuthBackend in this module.
+# * (And of course, register for a crypto key with the main authentication
+# provider website)
+# * If the application uses the django admin interface, the login screen
+# has to be replaced with something similar to login.html in this
+# directory (adjust urls, and name it admin/login.html in any template
+# directory that's processed before the default django.contrib.admin)
+#
+
+from django.http import HttpResponse, HttpResponseRedirect
+from django.contrib.auth.models import User
+from django.contrib.auth.backends import ModelBackend
+from django.contrib.auth import login as django_login
+from django.contrib.auth import logout as django_logout
+from django.conf import settings
+
+import base64
+import json
+import socket
+import urlparse
+import urllib
+from Crypto.Cipher import AES
+from Crypto.Hash import SHA
+from Crypto import Random
+import time
+
+class AuthBackend(ModelBackend):
+ # We declare a fake backend that always fails direct authentication -
+ # since we should never be using direct authentication in the first place!
+ def authenticate(self, username=None, password=None):
+ raise Exception("Direct authentication not supported")
+
+
+####
+# Two regular django views to interact with the login system
+####
+
+# Handle login requests by sending them off to the main site
+def login(request):
+ if request.GET.has_key('next'):
+ # Put together an url-encoded dict of parameters we're getting back,
+ # including a small nonce at the beginning to make sure it doesn't
+ # encrypt the same way every time.
+ s = "t=%s&%s" % (int(time.time()), urllib.urlencode({'r': request.GET['next']}))
+ # Now encrypt it
+ r = Random.new()
+ iv = r.read(16)
+ encryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16], AES.MODE_CBC, iv)
+ cipher = encryptor.encrypt(s + ' ' * (16-(len(s) % 16))) # pad to 16 bytes
+
+ return HttpResponseRedirect("%s?d=%s$%s" % (
+ settings.PGAUTH_REDIRECT,
+ base64.b64encode(iv, "-_"),
+ base64.b64encode(cipher, "-_"),
+ ))
+ else:
+ return HttpResponseRedirect(settings.PGAUTH_REDIRECT)
+
+# Handle logout requests by logging out of this site and then
+# redirecting to log out from the main site as well.
+def logout(request):
+ if request.user.is_authenticated():
+ django_logout(request)
+ return HttpResponseRedirect("%slogout/" % settings.PGAUTH_REDIRECT)
+
+# Receive an authentication response from the main website and try
+# to log the user in.
+def auth_receive(request):
+ if request.GET.has_key('s') and request.GET['s'] == "logout":
+ # This was a logout request
+ return HttpResponseRedirect('/')
+
+ if not request.GET.has_key('i'):
+ return HttpResponse("Missing IV in url!", status=400)
+ if not request.GET.has_key('d'):
+ return HttpResponse("Missing data in url!", status=400)
+
+ # Set up an AES object and decrypt the data we received
+ decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY),
+ AES.MODE_CBC,
+ base64.b64decode(str(request.GET['i']), "-_"))
+ s = decryptor.decrypt(base64.b64decode(str(request.GET['d']), "-_")).rstrip(' ')
+
+ # Now un-urlencode it
+ try:
+ data = urlparse.parse_qs(s, strict_parsing=True)
+ except ValueError:
+ return HttpResponse("Invalid encrypted data received.", status=400)
+
+ # Check the timestamp in the authentication
+ if (int(data['t'][0]) < time.time() - 10):
+ return HttpResponse("Authentication token too old.", status=400)
+
+ # Update the user record (if any)
+ try:
+ user = User.objects.get(username=data['u'][0])
+ # User found, let's see if any important fields have changed
+ changed = False
+ if user.first_name != data['f'][0]:
+ user.first_name = data['f'][0]
+ changed = True
+ if user.last_name != data['l'][0]:
+ user.last_name = data['l'][0]
+ changed = True
+ if user.email != data['e'][0]:
+ user.email = data['e'][0]
+ changed= True
+ if changed:
+ user.save()
+ except User.DoesNotExist:
+ # User not found, create it!
+
+ # NOTE! We have some legacy users where there is a user in
+ # the database with a different userid. Instead of trying to
+ # somehow fix that live, give a proper error message and
+ # have somebody look at it manually.
+ if User.objects.filter(email=data['e'][0]).exists():
+ return HttpResponse("""A user with email %s already exists, but with
+a different username than %s.
+
+This is almost certainly caused by some legacy data in our database.
+Please send an email to [email protected], indicating the username
+and email address from above, and we'll manually merge the two accounts
+for you.
+
+We apologize for the inconvenience.
+""" % (data['e'][0], data['u'][0]), content_type='text/plain')
+
+ user = User(username=data['u'][0],
+ first_name=data['f'][0],
+ last_name=data['l'][0],
+ email=data['e'][0],
+ password='setbypluginnotasha1',
+ )
+ user.save()
+
+ # Ok, we have a proper user record. Now tell django that
+ # we're authenticated so it persists it in the session. Before
+ # we do that, we have to annotate it with the backend information.
+ user.backend = "%s.%s" % (AuthBackend.__module__, AuthBackend.__name__)
+ django_login(request, user)
+
+ # Finally, check of we have a data package that tells us where to
+ # redirect the user.
+ if data.has_key('d'):
+ (ivs, datas) = data['d'][0].split('$')
+ decryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16],
+ AES.MODE_CBC,
+ base64.b64decode(ivs, "-_"))
+ s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ')
+ try:
+ rdata = urlparse.parse_qs(s, strict_parsing=True)
+ except ValueError:
+ return HttpResponse("Invalid encrypted data received.", status=400)
+ if rdata.has_key('r'):
+ # Redirect address
+ return HttpResponseRedirect(rdata['r'][0])
+ # No redirect specified, see if we have it in our settings
+ if hasattr(settings, 'PGAUTH_REDIRECT_SUCCESS'):
+ return HttpResponseRedirect(settings.PGAUTH_REDIRECT_SUCCESS)
+ return HttpResponse("Authentication successful, but don't know where to redirect!", status=500)
+
+
+# Perform a search in the central system. Note that the results are returned as an
+# array of dicts, and *not* as User objects. To be able to for example reference the
+# user through a ForeignKey, a User object must be materialized locally. We don't do
+# that here, as this search might potentially return a lot of unrelated users since
+# it's a wildcard match.
+# Unlike the authentication, searching does not involve the browser - we just make
+# a direct http call.
+def user_search(searchterm=None, userid=None):
+ # If upsteam isn't responding quickly, it's not going to respond at all, and
+ # 10 seconds is already quite long.
+ socket.setdefaulttimeout(10)
+ if userid:
+ q = {'u': userid}
+ else:
+ q = {'s': searchterm}
+
+ u = urllib.urlopen('%ssearch/?%s' % (
+ settings.PGAUTH_REDIRECT,
+ urllib.urlencode(q),
+ ))
+ (ivs, datas) = u.read().split('&')
+ u.close()
+
+ # Decryption time
+ decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY),
+ AES.MODE_CBC,
+ base64.b64decode(ivs, "-_"))
+ s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ')
+ j = json.loads(s)
+
+ return j
diff --git a/web/pgperffarm/settings.py b/web/pgperffarm/settings.py
index 8381541..29d74dc 100644
--- a/web/pgperffarm/settings.py
+++ b/web/pgperffarm/settings.py
@@ -106,5 +106,9 @@ STATICFILES_DIRS = [
"static/",
]
+AUTHENTICATION_BACKENDS = (
+ 'pgperffarm.auth.AuthBackend',
+)
+
# Load local settings overrides
from settings_local import *
diff --git a/web/pgperffarm/settings_local.py.in b/web/pgperffarm/settings_local.py.in
index fbe4ac6..39618d4 100644
--- a/web/pgperffarm/settings_local.py.in
+++ b/web/pgperffarm/settings_local.py.in
@@ -10,3 +10,5 @@ DATABASES={
}
}
+PGAUTH_REDIRECT=''
+PGAUTH_KEY='' \ No newline at end of file
diff --git a/web/pgperffarm/urls.py b/web/pgperffarm/urls.py
index e07097f..ea9962b 100644
--- a/web/pgperffarm/urls.py
+++ b/web/pgperffarm/urls.py
@@ -21,7 +21,12 @@ urlpatterns = [
url(r'^$', 'pgperffarm.views.index', name='index'),
url(r'^/licence$', 'pgperffarm.views.licence', name='licence'),
url(r'^/ppolicy$', 'pgperffarm.views.ppolicy', name='ppolicy'),
-
+
+ # Auth system integration
+ url(r'^(?:account/)?login/?$', 'pgperffarm.auth.login'),
+ url(r'^(?:account/)?logout/?$', 'pgperffarm.auth.logout'),
+ url(r'^auth_receive/$', 'pgperffarm.auth.auth_receive'),
+
# Admin site
url(r'^admin/', include(admin.site.urls)),
diff --git a/web/pgperffarm/views.py b/web/pgperffarm/views.py
index ecdf975..7fdcf34 100644
--- a/web/pgperffarm/views.py
+++ b/web/pgperffarm/views.py
@@ -1,17 +1,19 @@
"""Views for the core PGPerfFarm app"""
from django.shortcuts import render_to_response
-from django.http import HttpResponse, Http404
-from django.template import TemplateDoesNotExist, loader, Context
+from django.template import RequestContext
import datetime
# Handle the static pages
def index(request):
- return render_to_response('index.html')
+ return render_to_response('index.html',
+ context_instance=RequestContext(request))
def licence(request):
- return render_to_response('licence.html')
+ return render_to_response('licence.html',
+ context_instance=RequestContext(request))
def ppolicy(request):
- return render_to_response('ppolicy.html') \ No newline at end of file
+ return render_to_response('ppolicy.html',
+ context_instance=RequestContext(request)) \ No newline at end of file
diff --git a/web/templates/admin/login.html b/web/templates/admin/login.html
new file mode 100644
index 0000000..e9e7257
--- /dev/null
+++ b/web/templates/admin/login.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<meta http-equiv="refresh" content="0;url={% url 'pgperffarm.auth.login' %}/?next=/admin/"></meta>
+</head>
+<body>
+<h1>Redirect</h1>
+<p>
+Redirect <a href="{% url 'pgperffarm.auth.login' %}/?next=/admin/">here</a>
+</p>
+</body>
+</html> \ No newline at end of file
diff --git a/web/templates/body.html b/web/templates/body.html
index 7e06dad..08313b5 100644
--- a/web/templates/body.html
+++ b/web/templates/body.html
@@ -38,16 +38,19 @@
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
+ {% if user.is_authenticated %}
<li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">{% gravatar user.email 18 %} {{ request.user }}<b class="caret"></b></a>
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">{% gravatar user.email 18 %} {{ user }}<b class="caret"></b></a>
<ul class="dropdown-menu">
- {% if request.user.is_superuser %}
+ {% if user.is_staff %}
<li><a href="{% url 'admin:index' %}">Admin Site</a></li>
- <li><a href="{% url 'django-admindocs-docroot' %}">Admin Docs</a></li>
{% endif %}
- <li><a href="#">Logout</a></li>
+ <li><a href="{% url 'pgperffarm.auth.logout' %}">Logout</a></li>
</ul>
</li>
+ {% else %}
+ <li><a href="{% url 'pgperffarm.auth.login' %}/?next={{ request.path }}">Login</a></li>
+ {% endif %}
</ul>
</div>
</div>