15
15
from enum import Enum
16
16
import json
17
17
from functools import wraps
18
+ from hashlib import md5
18
19
from django .conf .urls import url as re_path
19
- from django .contrib .auth .hashers import check_password
20
+ from django .contrib .auth .hashers import check_password as django_check_password
21
+ from django .contrib .auth .hashers import is_password_usable
20
22
from django .db .models import Q
21
23
from django .http import JsonResponse
22
24
from django .views .decorators .csrf import csrf_exempt
29
31
)
30
32
from pycon .settings import MATRIX_AUTH_API_DEBUG as DEBUG
31
33
from pycon .settings import MATRIX_AUTH_API_ALLOWED_IPS as ALLOWED_IPS
34
+ from pycon .settings import SECRET_KEY
32
35
33
36
34
37
# Error Codes
@@ -113,6 +116,57 @@ def wrapper(request, *args, **kwargs):
113
116
return wrapper
114
117
115
118
119
+ def check_user_password (user , password ):
120
+ # Two options: either our User has a valid password, in which case we do
121
+ # check it, or not, in which case we check it against the generated passwd.
122
+ if not is_password_usable (user .password ):
123
+ return password == generate_matrix_password (user )
124
+ return django_check_password (password , user .password )
125
+
126
+
127
+ def get_assigned_tickets (user , conference ):
128
+ return Ticket .objects .filter (
129
+ Q (fare__conference = conference .code )
130
+ & Q (frozen = False ) # i.e. the ticket was not cancelled
131
+ & Q (orderitem__order___complete = True ) # i.e. they paid
132
+ & Q (user = user ) # i.e. assigned to user
133
+ )
134
+
135
+
136
+ def is_speaker (user , conference ):
137
+ # A speaker is a user with at least one accepted talk in the current
138
+ # conference.
139
+ try :
140
+ speaker = user .speaker
141
+ except Speaker .DoesNotExist :
142
+ return False
143
+ return TalkSpeaker .objects .filter (
144
+ speaker = speaker ,
145
+ talk__conference = conference .code ,
146
+ talk__status = 'accepted'
147
+ ).count () > 0
148
+
149
+
150
+ def generate_matrix_password (user ):
151
+ """
152
+ Create a temporary password for `user` to that they can login into our
153
+ matrix chat server using their email address and that password. This is
154
+ only needed for social auth users since they do not have a valid password
155
+ in our database.
156
+
157
+ The generated passowrd is not stored anywhere.
158
+ """
159
+ def n_base_b (n , b , nums = '0123456789abcdefghijklmnopqrstuvwxyz' ):
160
+ """Return `n` in base `b`."""
161
+
162
+ return ((n == 0 ) and nums [0 ]) or \
163
+ (n_base_b (n // b , b , nums ).lstrip (nums [0 ]) + nums [n % b ])
164
+
165
+ encoded = md5 (str (user .email + SECRET_KEY ).encode ()).hexdigest ()
166
+ n = int (encoded , 16 )
167
+ return n_base_b (n , 36 )
168
+
169
+
116
170
@csrf_exempt
117
171
@ensure_post
118
172
@ensure_https_in_ops
@@ -130,6 +184,13 @@ def isauth(request):
130
184
"password": str (not encrypted)
131
185
}
132
186
187
+ or
188
+
189
+ {
190
+ "username": str,
191
+ "password": str (not encrypted)
192
+ }
193
+
133
194
Output (JSON)
134
195
{
135
196
"username": str,
@@ -153,62 +214,55 @@ def isauth(request):
153
214
"error": int
154
215
}
155
216
"""
156
- required_fields = {'email' , 'password' }
157
-
158
217
try :
159
218
data = json .loads (request .body )
160
219
except json .decoder .JSONDecodeError as ex :
161
220
return _error (ApiError .INPUT_ERROR , ex .msg )
162
221
163
- if not isinstance (data , dict ) or not required_fields . issubset ( data . keys ()) :
222
+ if not isinstance (data , dict ):
164
223
return _error (ApiError .INPUT_ERROR ,
165
224
'please provide credentials in JSON format' )
166
-
167
- # First, let's find the user/account profile given the email address
168
- try :
169
- profile = AttendeeProfile .objects .get (user__email = data ['email' ])
170
- except AttendeeProfile .DoesNotExist :
171
- return _error (ApiError .AUTH_ERROR , 'unknown user' )
225
+ if 'password' not in data :
226
+ return _error (ApiError .INPUT_ERROR ,
227
+ 'please provide user password in JSON payload' )
228
+ if 'username' not in data and 'email' not in data :
229
+ return _error (ApiError .INPUT_ERROR ,
230
+ 'please provide username or email in JSON payload' )
231
+
232
+ # First, let's find the user/account profile given the email/username as
233
+ # appropriate.
234
+ if 'email' in data :
235
+ try :
236
+ profile = AttendeeProfile .objects .get (user__email = data ['email' ])
237
+ except AttendeeProfile .DoesNotExist :
238
+ return _error (ApiError .AUTH_ERROR , 'unknown user' )
239
+ elif 'username' in data :
240
+ try :
241
+ profile = AttendeeProfile .objects .get (
242
+ user__username = data ['username' ]
243
+ )
244
+ except AttendeeProfile .DoesNotExist :
245
+ return _error (ApiError .AUTH_ERROR , 'unknown user' )
246
+ else :
247
+ return _error (ApiError .INPUT_ERROR , 'no email/username provided' )
172
248
173
249
# Is the password OK?
174
- if not check_password ( data ['password' ], profile . user . password ):
250
+ if not check_user_password ( profile . user , data ['password' ]):
175
251
return _error (ApiError .AUTH_ERROR , 'authentication error' )
176
252
177
- # Get the tickets **assigned** to the user
178
253
conference = Conference .objects .current ()
179
-
180
- tickets = Ticket .objects .filter (
181
- Q (fare__conference = conference .code )
182
- & Q (frozen = False ) # i.e. the ticket was not cancelled
183
- & Q (orderitem__order___complete = True ) # i.e. they paid
184
- & Q (user = profile .user ) # i.e. assigned to user
185
- )
186
-
187
- # A speaker is a user with at least one accepted talk in the current
188
- # conference.
189
- try :
190
- speaker = profile .user .speaker
191
- except Speaker .DoesNotExist :
192
- is_speaker = False
193
- else :
194
- is_speaker = TalkSpeaker .objects .filter (
195
- speaker = speaker ,
196
- talk__conference = conference .code ,
197
- talk__status = 'accepted'
198
- ).count () > 0
199
-
200
254
payload = {
201
255
"username" : profile .user .username ,
202
256
"first_name" : profile .user .first_name ,
203
257
"last_name" : profile .user .last_name ,
204
258
"email" : profile .user .email ,
205
259
"is_staff" : profile .user .is_staff ,
206
- "is_speaker" : is_speaker ,
260
+ "is_speaker" : is_speaker ( profile . user , conference ) ,
207
261
"is_active" : profile .user .is_active ,
208
262
"is_minor" : profile .is_minor ,
209
263
"tickets" : [
210
264
{"fare_name" : t .fare .name , "fare_code" : t .fare .code }
211
- for t in tickets
265
+ for t in get_assigned_tickets ( profile . user , conference )
212
266
]
213
267
}
214
268
0 commit comments