8
8
# * Make sure the view "login" from this module is used for login
9
9
# * Map an url somwehere (typically /auth_receive/) to the auth_receive
10
10
# 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.
11
15
# * In settings.py, set AUTHENTICATION_BACKENDS to point to the class
12
16
# AuthBackend in this module.
13
17
# * (And of course, register for a crypto key with the main authentication
19
23
#
20
24
21
25
from django .http import HttpResponse , HttpResponseRedirect
26
+ from django .views .decorators .csrf import csrf_exempt
22
27
from django .contrib .auth .models import User
23
28
from django .contrib .auth .backends import ModelBackend
24
29
from django .contrib .auth import login as django_login
25
30
from django .contrib .auth import logout as django_logout
31
+ from django .dispatch import Signal
32
+ from django .db import transaction
26
33
from django .conf import settings
27
34
28
35
import base64
29
36
import json
30
37
import socket
31
- from urllib .parse import urlparse , urlencode , parse_qs , quote_plus
38
+ import hmac
39
+ from urllib .parse import urlencode , parse_qs
32
40
import requests
33
41
from Cryptodome .Cipher import AES
34
42
from Cryptodome .Hash import SHA
35
43
from Cryptodome import Random
36
44
import time
37
45
38
46
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
+
39
53
class AuthBackend (ModelBackend ):
40
54
# We declare a fake backend that always fails direct authentication -
41
55
# since we should never be using direct authentication in the first place!
@@ -53,7 +67,7 @@ def login(request):
53
67
# Put together an url-encoded dict of parameters we're getting back,
54
68
# including a small nonce at the beginning to make sure it doesn't
55
69
# 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' ]}))
57
71
# Now encrypt it
58
72
r = Random .new ()
59
73
iv = r .read (16 )
@@ -109,18 +123,18 @@ def auth_receive(request):
109
123
try :
110
124
user = User .objects .get (username = data ['u' ][0 ])
111
125
# User found, let's see if any important fields have changed
112
- changed = False
126
+ changed = []
113
127
if user .first_name != data ['f' ][0 ]:
114
128
user .first_name = data ['f' ][0 ]
115
- changed = True
129
+ changed . append ( 'first_name' )
116
130
if user .last_name != data ['l' ][0 ]:
117
131
user .last_name = data ['l' ][0 ]
118
- changed = True
132
+ changed . append ( 'last_name' )
119
133
if user .email != data ['e' ][0 ]:
120
134
user .email = data ['e' ][0 ]
121
- changed = True
135
+ changed . append ( 'email' )
122
136
if changed :
123
- user .save ()
137
+ user .save (update_fields = changed )
124
138
except User .DoesNotExist :
125
139
# User not found, create it!
126
140
@@ -166,6 +180,11 @@ def auth_receive(request):
166
180
user .backend = "%s.%s" % (AuthBackend .__module__ , AuthBackend .__name__ )
167
181
django_login (request , user )
168
182
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
+
169
188
# Finally, check of we have a data package that tells us where to
170
189
# redirect the user.
171
190
if 'd' in data :
@@ -187,6 +206,73 @@ def auth_receive(request):
187
206
return HttpResponse ("Authentication successful, but don't know where to redirect!" , status = 500 )
188
207
189
208
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
+
190
276
# Perform a search in the central system. Note that the results are returned as an
191
277
# array of dicts, and *not* as User objects. To be able to for example reference the
192
278
# user through a ForeignKey, a User object must be materialized locally. We don't do
@@ -240,9 +326,13 @@ def user_import(uid):
240
326
if User .objects .filter (username = u ['u' ]).exists ():
241
327
raise Exception ("User already exists" )
242
328
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