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
1923#
2024
2125from django .http import HttpResponse , HttpResponseRedirect
26+ from django .views .decorators .csrf import csrf_exempt
2227from django .contrib .auth .models import User
2328from django .contrib .auth .backends import ModelBackend
2429from django .contrib .auth import login as django_login
2530from django .contrib .auth import logout as django_logout
31+ from django .dispatch import Signal
32+ from django .db import transaction
2633from django .conf import settings
2734
2835import base64
2936import json
3037import socket
31- from urllib .parse import urlparse , urlencode , parse_qs
38+ import hmac
39+ from urllib .parse import urlencode , parse_qs
3240import requests
3341from Cryptodome .Cipher import AES
3442from Cryptodome .Hash import SHA
3543from Cryptodome import Random
3644import 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+
3953class 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!
@@ -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
0 commit comments