1515from enum import Enum
1616import json
1717from functools import wraps
18+ from hashlib import md5
1819from 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
2022from django .db .models import Q
2123from django .http import JsonResponse
2224from django .views .decorators .csrf import csrf_exempt
2931)
3032from pycon .settings import MATRIX_AUTH_API_DEBUG as DEBUG
3133from pycon .settings import MATRIX_AUTH_API_ALLOWED_IPS as ALLOWED_IPS
34+ from pycon .settings import SECRET_KEY
3235
3336
3437# Error Codes
@@ -113,6 +116,57 @@ def wrapper(request, *args, **kwargs):
113116 return wrapper
114117
115118
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+
116170@csrf_exempt
117171@ensure_post
118172@ensure_https_in_ops
@@ -130,6 +184,13 @@ def isauth(request):
130184 "password": str (not encrypted)
131185 }
132186
187+ or
188+
189+ {
190+ "username": str,
191+ "password": str (not encrypted)
192+ }
193+
133194 Output (JSON)
134195 {
135196 "username": str,
@@ -153,62 +214,55 @@ def isauth(request):
153214 "error": int
154215 }
155216 """
156- required_fields = {'email' , 'password' }
157-
158217 try :
159218 data = json .loads (request .body )
160219 except json .decoder .JSONDecodeError as ex :
161220 return _error (ApiError .INPUT_ERROR , ex .msg )
162221
163- if not isinstance (data , dict ) or not required_fields . issubset ( data . keys ()) :
222+ if not isinstance (data , dict ):
164223 return _error (ApiError .INPUT_ERROR ,
165224 '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' )
172248
173249 # Is the password OK?
174- if not check_password ( data ['password' ], profile . user . password ):
250+ if not check_user_password ( profile . user , data ['password' ]):
175251 return _error (ApiError .AUTH_ERROR , 'authentication error' )
176252
177- # Get the tickets **assigned** to the user
178253 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-
200254 payload = {
201255 "username" : profile .user .username ,
202256 "first_name" : profile .user .first_name ,
203257 "last_name" : profile .user .last_name ,
204258 "email" : profile .user .email ,
205259 "is_staff" : profile .user .is_staff ,
206- "is_speaker" : is_speaker ,
260+ "is_speaker" : is_speaker ( profile . user , conference ) ,
207261 "is_active" : profile .user .is_active ,
208262 "is_minor" : profile .is_minor ,
209263 "tickets" : [
210264 {"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 )
212266 ]
213267 }
214268
0 commit comments