Add integration with community auth system
authorMagnus Hagander <magnus@hagander.net>
Tue, 22 Apr 2014 11:44:25 +0000 (13:44 +0200)
committerMagnus Hagander <magnus@hagander.net>
Tue, 22 Apr 2014 11:44:25 +0000 (13:44 +0200)
pgcommitfest/auth.py [new file with mode: 0644]
pgcommitfest/commitfest/templates/base.html
pgcommitfest/urls.py

diff --git a/pgcommitfest/auth.py b/pgcommitfest/auth.py
new file mode 100644 (file)
index 0000000..ecb3d48
--- /dev/null
@@ -0,0 +1,170 @@
+#
+# 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 (typicall /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 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'):
+               raise Exception("Missing IV")
+       if not request.GET.has_key('d'):
+               raise Exception("Missing data!")
+
+       # 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, e:
+               raise Exception("Invalid encrypted data received.")
+
+       # Check the timestamp in the authentication
+       if (int(data['t'][0]) < time.time() - 10):
+               raise Exception("Authentication token too old.")
+
+       # 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, e:
+               # 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 webmaster@postgresql.eu, indicating the username
+and email address from above, and we'll manually marge 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:
+                       raise Exception("Invalid encrypted data received.")
+               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)
+       raise Exception("Authentication successful, but don't know where to redirect!")
index 5d6f4058b327f5248387e3eb5d1ec5977cdcb1ac..9495cbbe8c54ab4a8521631308fd171e61c3cc76 100644 (file)
@@ -16,7 +16,7 @@
 {%endfor%}
  <li class="active">{{title}}</li>
 {%if user.is_authenticated%}
- <li class="pull-right active"><a href="/account/profile/">Logged in</a> as mha (<a href="/account/logout/">log out</a>)</li>
+ <li class="pull-right active">Logged in as mha (<a href="/account/logout/">log out</a>)</li>
 {%endif%}
 </ul>
 
index 106aeb13bbc90ce07b38a762cb2e250b43160084..7eb3576523f007ae4ed9be133f1304128004cbc7 100644 (file)
@@ -19,6 +19,11 @@ urlpatterns = patterns('',
 
     url(r'^selectable/', include('selectable.urls')),
 
+    # Auth system integration
+    (r'^(?:account/)?login/?$', 'auth.login'),
+    (r'^(?:account/)?logout/?$', 'auth.logout'),
+    (r'^auth_receive/$', 'auth.auth_receive'),
+
     # Examples:
     # url(r'^$', 'pgcommitfest.views.home', name='home'),
     # url(r'^pgcommitfest/', include('pgcommitfest.foo.urls')),