summaryrefslogtreecommitdiff
path: root/postgresqleu/auth.py
diff options
context:
space:
mode:
authorMagnus Hagander2020-08-11 16:00:53 +0000
committerMagnus Hagander2020-08-11 16:00:53 +0000
commit064dd3f9e5308e8f9445a5e6a8dabf4580c35c8f (patch)
tree273e485869d70cfd490e3b968faa650266683a68 /postgresqleu/auth.py
parent92965368413e6ff164130f8baf4bf7ffdb9b2c39 (diff)
Import latest version of community auth plugin
This includes an URL endpoint to receive push updates (enabled inividually for each instance) when using postgresql.org community authentication.
Diffstat (limited to 'postgresqleu/auth.py')
-rw-r--r--postgresqleu/auth.py114
1 files changed, 102 insertions, 12 deletions
diff --git a/postgresqleu/auth.py b/postgresqleu/auth.py
index 55eb9b64..e28aa9d1 100644
--- a/postgresqleu/auth.py
+++ b/postgresqleu/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,15 +23,19 @@
#
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
+import hmac
from urllib.parse import urlencode, parse_qs
import requests
from Cryptodome.Cipher import AES
@@ -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
@@ -194,7 +280,7 @@ We apologize for the inconvenience.
# 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):
+def user_search(searchterm=None, email=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)
@@ -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