summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMagnus Hagander2020-08-11 11:17:30 +0000
committerMagnus Hagander2020-08-11 12:04:24 +0000
commit86725ca155503df9e88482fd8f8eb690c3eadc23 (patch)
treeb7f4157e63674cd9c4c3dc74b98b02f26834a6f2
parent5e3b6297580bca40973e0b446035286a70bf9ad5 (diff)
Import latest version of community authentication plugin
This includes now accepting push changes from upstream.
-rw-r--r--pgcommitfest/auth.py114
-rw-r--r--pgcommitfest/urls.py1
2 files changed, 103 insertions, 12 deletions
diff --git a/pgcommitfest/auth.py b/pgcommitfest/auth.py
index 87ffb0b..d7bd25c 100644
--- a/pgcommitfest/auth.py
+++ b/pgcommitfest/auth.py
@@ -8,6 +8,10 @@
# * Make sure the view "login" from this module is used for login
# * Map an url somwehere (typically /auth_receive/) to the auth_receive
# view.
+# * To receive live updates (not just during login), map an url somewhere
+# (typically /auth_api/) to the auth_api view.
+# * To receive live updates, also connect to the signal auth_user_data_received.
+# This signal will fire *both* on login events *and* on background updates.
# * 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
@@ -19,16 +23,20 @@
#
from django.http import HttpResponse, HttpResponseRedirect
+from django.views.decorators.csrf import csrf_exempt
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.dispatch import Signal
+from django.db import transaction
from django.conf import settings
import base64
import json
import socket
-from urllib.parse import urlparse, urlencode, parse_qs
+import hmac
+from urllib.parse import urlencode, parse_qs
import requests
from Cryptodome.Cipher import AES
from Cryptodome.Hash import SHA
@@ -36,6 +44,12 @@ from Cryptodome import Random
import time
+# This signal fires whenever new user data has been received. Note that this
+# happens *after* first_name, last_name and email has been updated on the user
+# record, so those are not included in the userdata struct.
+auth_user_data_received = Signal(providing_args=['user', 'userdata'])
+
+
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!
@@ -109,18 +123,18 @@ def auth_receive(request):
try:
user = User.objects.get(username=data['u'][0])
# User found, let's see if any important fields have changed
- changed = False
+ changed = []
if user.first_name != data['f'][0]:
user.first_name = data['f'][0]
- changed = True
+ changed.append('first_name')
if user.last_name != data['l'][0]:
user.last_name = data['l'][0]
- changed = True
+ changed.append('last_name')
if user.email != data['e'][0]:
user.email = data['e'][0]
- changed = True
+ changed.append('email')
if changed:
- user.save()
+ user.save(update_fields=changed)
except User.DoesNotExist:
# User not found, create it!
@@ -166,6 +180,11 @@ We apologize for the inconvenience.
user.backend = "%s.%s" % (AuthBackend.__module__, AuthBackend.__name__)
django_login(request, user)
+ # Signal that we have information about this user
+ auth_user_data_received.send(sender=auth_receive, user=user, userdata={
+ 'secondaryemails': data['se'][0].split(',') if 'se' in data else []
+ })
+
# Finally, check of we have a data package that tells us where to
# redirect the user.
if 'd' in data:
@@ -187,6 +206,73 @@ We apologize for the inconvenience.
return HttpResponse("Authentication successful, but don't know where to redirect!", status=500)
+# Receive API calls from upstream, such as push changes to users
+@csrf_exempt
+def auth_api(request):
+ if 'X-pgauth-sig' not in request.headers:
+ return HttpResponse("Missing signature header!", status=400)
+
+ try:
+ sig = base64.b64decode(request.headers['X-pgauth-sig'])
+ except Exception:
+ return HttpResponse("Invalid signature header!", status=400)
+
+ try:
+ h = hmac.digest(
+ base64.b64decode(settings.PGAUTH_KEY),
+ msg=request.body,
+ digest='sha512',
+ )
+ if not hmac.compare_digest(h, sig):
+ return HttpResponse("Invalid signature!", status=401)
+ except Exception:
+ return HttpResponse("Unable to compute hmac", status=400)
+
+ try:
+ pushstruct = json.loads(request.body)
+ except Exception:
+ return HttpResponse("Invalid JSON!", status=400)
+
+ def _conditionally_update_record(rectype, recordkey, structkey, fieldmap, struct):
+ try:
+ obj = rectype.objects.get(**{recordkey: struct[structkey]})
+ ufields = []
+ for k, v in fieldmap.items():
+ if struct[k] != getattr(obj, v):
+ setattr(obj, v, struct[k])
+ ufields.append(v)
+ if ufields:
+ obj.save(update_fields=ufields)
+ return obj
+ except rectype.DoesNotExist:
+ # If the record doesn't exist, we just ignore it
+ return None
+
+ # Process the received structure
+ if pushstruct.get('type', None) == 'update':
+ # Process updates!
+ with transaction.atomic():
+ for u in pushstruct.get('users', []):
+ user = _conditionally_update_record(
+ User,
+ 'username', 'username',
+ {
+ 'firstname': 'first_name',
+ 'lastname': 'last_name',
+ 'email': 'email',
+ },
+ u,
+ )
+
+ # Signal that we have information about this user (only if it exists)
+ if user:
+ auth_user_data_received.send(sender=auth_api, user=user, userdata={
+ k: u[k] for k in u.keys() if k not in ['firstname', 'lastname', 'email', ]
+ })
+
+ return HttpResponse("OK", status=200)
+
+
# 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
@@ -240,9 +326,13 @@ def user_import(uid):
if User.objects.filter(username=u['u']).exists():
raise Exception("User already exists")
- User(username=u['u'],
- first_name=u['f'],
- last_name=u['l'],
- email=u['e'],
- password='setbypluginnotsha1',
- ).save()
+ u = User(
+ username=u['u'],
+ first_name=u['f'],
+ last_name=u['l'],
+ email=u['e'],
+ password='setbypluginnotsha1',
+ )
+ u.save()
+
+ return u
diff --git a/pgcommitfest/urls.py b/pgcommitfest/urls.py
index cb42554..ec013f4 100644
--- a/pgcommitfest/urls.py
+++ b/pgcommitfest/urls.py
@@ -40,6 +40,7 @@ urlpatterns = [
url(r'^(?:account/)?login/?$', pgcommitfest.auth.login),
url(r'^(?:account/)?logout/?$', pgcommitfest.auth.logout),
url(r'^auth_receive/$', pgcommitfest.auth.auth_receive),
+ url(r'^auth_api/$', pgcommitfest.auth.auth_api),
# Account management
url(r'^account/profile/$', pgcommitfest.userprofile.views.userprofile),