Skip to content

Commit f91d2ba

Browse files
committed
Update to latest version of community auth plugin
This includes support for receiving updates through the push api.
1 parent 3cb4a4c commit f91d2ba

File tree

2 files changed

+104
-13
lines changed

2 files changed

+104
-13
lines changed

django/archives/auth.py

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
# * Make sure the view "login" from this module is used for login
99
# * Map an url somwehere (typically /auth_receive/) to the auth_receive
1010
# view.
11+
# * To receive live updates (not just during login), map an url somewhere
12+
# (typically /auth_api/) to the auth_api view.
13+
# * To receive live updates, also connect to the signal auth_user_data_received.
14+
# This signal will fire *both* on login events *and* on background updates.
1115
# * In settings.py, set AUTHENTICATION_BACKENDS to point to the class
1216
# AuthBackend in this module.
1317
# * (And of course, register for a crypto key with the main authentication
@@ -19,23 +23,33 @@
1923
#
2024

2125
from django.http import HttpResponse, HttpResponseRedirect
26+
from django.views.decorators.csrf import csrf_exempt
2227
from django.contrib.auth.models import User
2328
from django.contrib.auth.backends import ModelBackend
2429
from django.contrib.auth import login as django_login
2530
from django.contrib.auth import logout as django_logout
31+
from django.dispatch import Signal
32+
from django.db import transaction
2633
from django.conf import settings
2734

2835
import base64
2936
import json
3037
import socket
31-
from urllib.parse import urlparse, urlencode, parse_qs, quote_plus
38+
import hmac
39+
from urllib.parse import urlencode, parse_qs
3240
import requests
3341
from Cryptodome.Cipher import AES
3442
from Cryptodome.Hash import SHA
3543
from Cryptodome import Random
3644
import time
3745

3846

47+
# This signal fires whenever new user data has been received. Note that this
48+
# happens *after* first_name, last_name and email has been updated on the user
49+
# record, so those are not included in the userdata struct.
50+
auth_user_data_received = Signal(providing_args=['user', 'userdata'])
51+
52+
3953
class AuthBackend(ModelBackend):
4054
# We declare a fake backend that always fails direct authentication -
4155
# since we should never be using direct authentication in the first place!
@@ -53,7 +67,7 @@ def login(request):
5367
# Put together an url-encoded dict of parameters we're getting back,
5468
# including a small nonce at the beginning to make sure it doesn't
5569
# encrypt the same way every time.
56-
s = "t=%s&%s" % (int(time.time()), urlencode({'r': quote_plus(request.GET['next'], safe='/')}))
70+
s = "t=%s&%s" % (int(time.time()), urlencode({'r': request.GET['next']}))
5771
# Now encrypt it
5872
r = Random.new()
5973
iv = r.read(16)
@@ -109,18 +123,18 @@ def auth_receive(request):
109123
try:
110124
user = User.objects.get(username=data['u'][0])
111125
# User found, let's see if any important fields have changed
112-
changed = False
126+
changed = []
113127
if user.first_name != data['f'][0]:
114128
user.first_name = data['f'][0]
115-
changed = True
129+
changed.append('first_name')
116130
if user.last_name != data['l'][0]:
117131
user.last_name = data['l'][0]
118-
changed = True
132+
changed.append('last_name')
119133
if user.email != data['e'][0]:
120134
user.email = data['e'][0]
121-
changed = True
135+
changed.append('email')
122136
if changed:
123-
user.save()
137+
user.save(update_fields=changed)
124138
except User.DoesNotExist:
125139
# User not found, create it!
126140

@@ -166,6 +180,11 @@ def auth_receive(request):
166180
user.backend = "%s.%s" % (AuthBackend.__module__, AuthBackend.__name__)
167181
django_login(request, user)
168182

183+
# Signal that we have information about this user
184+
auth_user_data_received.send(sender=auth_receive, user=user, userdata={
185+
'secondaryemails': data['se'][0].split(',') if 'se' in data else []
186+
})
187+
169188
# Finally, check of we have a data package that tells us where to
170189
# redirect the user.
171190
if 'd' in data:
@@ -187,6 +206,73 @@ def auth_receive(request):
187206
return HttpResponse("Authentication successful, but don't know where to redirect!", status=500)
188207

189208

209+
# Receive API calls from upstream, such as push changes to users
210+
@csrf_exempt
211+
def auth_api(request):
212+
if 'X-pgauth-sig' not in request.headers:
213+
return HttpResponse("Missing signature header!", status=400)
214+
215+
try:
216+
sig = base64.b64decode(request.headers['X-pgauth-sig'])
217+
except Exception:
218+
return HttpResponse("Invalid signature header!", status=400)
219+
220+
try:
221+
h = hmac.digest(
222+
base64.b64decode(settings.PGAUTH_KEY),
223+
msg=request.body,
224+
digest='sha512',
225+
)
226+
if not hmac.compare_digest(h, sig):
227+
return HttpResponse("Invalid signature!", status=401)
228+
except Exception:
229+
return HttpResponse("Unable to compute hmac", status=400)
230+
231+
try:
232+
pushstruct = json.loads(request.body)
233+
except Exception:
234+
return HttpResponse("Invalid JSON!", status=400)
235+
236+
def _conditionally_update_record(rectype, recordkey, structkey, fieldmap, struct):
237+
try:
238+
obj = rectype.objects.get(**{recordkey: struct[structkey]})
239+
ufields = []
240+
for k, v in fieldmap.items():
241+
if struct[k] != getattr(obj, v):
242+
setattr(obj, v, struct[k])
243+
ufields.append(v)
244+
if ufields:
245+
obj.save(update_fields=ufields)
246+
return obj
247+
except rectype.DoesNotExist:
248+
# If the record doesn't exist, we just ignore it
249+
return None
250+
251+
# Process the received structure
252+
if pushstruct.get('type', None) == 'update':
253+
# Process updates!
254+
with transaction.atomic():
255+
for u in pushstruct.get('users', []):
256+
user = _conditionally_update_record(
257+
User,
258+
'username', 'username',
259+
{
260+
'firstname': 'first_name',
261+
'lastname': 'last_name',
262+
'email': 'email',
263+
},
264+
u,
265+
)
266+
267+
# Signal that we have information about this user (only if it exists)
268+
if user:
269+
auth_user_data_received.send(sender=auth_api, user=user, userdata={
270+
k: u[k] for k in u.keys() if k not in ['firstname', 'lastname', 'email', ]
271+
})
272+
273+
return HttpResponse("OK", status=200)
274+
275+
190276
# Perform a search in the central system. Note that the results are returned as an
191277
# array of dicts, and *not* as User objects. To be able to for example reference the
192278
# user through a ForeignKey, a User object must be materialized locally. We don't do
@@ -240,9 +326,13 @@ def user_import(uid):
240326
if User.objects.filter(username=u['u']).exists():
241327
raise Exception("User already exists")
242328

243-
User(username=u['u'],
244-
first_name=u['f'],
245-
last_name=u['l'],
246-
email=u['e'],
247-
password='setbypluginnotsha1',
248-
).save()
329+
u = User(
330+
username=u['u'],
331+
first_name=u['f'],
332+
last_name=u['l'],
333+
email=u['e'],
334+
password='setbypluginnotsha1',
335+
)
336+
u.save()
337+
338+
return u

django/archives/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,5 @@
7272
url(r'^(?:list/_auth/)?accounts/login/?$', archives.auth.login),
7373
url(r'^(?:list/_auth/)?accounts/logout/?$', archives.auth.logout),
7474
url(r'^(?:list/_auth/)?auth_receive/$', archives.auth.auth_receive),
75+
url(r'^(?:list/_auth/)?auth_api/$', archives.auth.auth_api),
7576
]

0 commit comments

Comments
 (0)