Import latest version of community authentication plugin
authorMagnus Hagander <[email protected]>
Tue, 11 Aug 2020 11:17:30 +0000 (13:17 +0200)
committerMagnus Hagander <[email protected]>
Tue, 11 Aug 2020 12:04:24 +0000 (14:04 +0200)
This includes now accepting push changes from upstream.

pgcommitfest/auth.py
pgcommitfest/urls.py

index 87ffb0b2adbcd45d956b7f625dc9ae29c7807bfa..d7bd25cb61cc604f6898ac23be4150222975769f 100644 (file)
@@ -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
 #
 
 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
index cb425547e4fcee97d1ae0dcf2e75749b55eb5c85..ec013f44ef5f8e17f9047e674fd170d95b1788ab 100644 (file)
@@ -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),