@@ -71,6 +71,7 @@ def validate_login(
7171 username : str ,
7272 password : str ,
7373 ip_address : AnyIPAddress ,
74+ admin_token : str = ""
7475) -> tuple [Participation | None , bytes | None ]:
7576 """Authenticate a user logging in, with username and password.
7677
@@ -91,6 +92,7 @@ def validate_login(
9192 username: the username the user provided.
9293 password: the password the user provided.
9394 ip_address: the IP address the request came from.
95+ admin_token: administrator's token used to impersonate a user
9496
9597 return: if the user couldn't
9698 be authenticated then return None, otherwise return the
@@ -103,7 +105,7 @@ def log_failed_attempt(msg, *args):
103105 "%r, on contest %s, at %s: " + msg , ip_address ,
104106 username , contest .name , timestamp , * args )
105107
106- if not contest .allow_password_authentication :
108+ if not contest .allow_password_authentication and admin_token == "" :
107109 log_failed_attempt ("password authentication not allowed" )
108110 return None , None
109111
@@ -120,6 +122,20 @@ def log_failed_attempt(msg, *args):
120122 log_failed_attempt ("user not registered to contest" )
121123 return None , None
122124
125+ if admin_token != "" :
126+ if (config .contest_admin_token is not None
127+ and admin_token != config .contest_admin_token ):
128+ log_failed_attempt ("invalid admin token" )
129+ return None , None
130+
131+ logger .info ("Successful impersonated login from IP address %s, as user %r, on "
132+ "contest %s, at %s" , ip_address , username , contest .name ,
133+ timestamp )
134+
135+ return (participation ,
136+ json .dumps ([username , "" , make_timestamp (timestamp ), True ])
137+ .encode ("utf-8" ))
138+
123139 correct_password = get_password (participation )
124140
125141 try :
@@ -151,7 +167,7 @@ def log_failed_attempt(msg, *args):
151167 # If hashing is used, the cookie stores the hashed password so that
152168 # the expensive bcrypt call doesn't need to be done at every request.
153169 return (participation ,
154- json .dumps ([username , correct_password , make_timestamp (timestamp )])
170+ json .dumps ([username , correct_password , make_timestamp (timestamp ), False ])
155171 .encode ("utf-8" ))
156172
157173
@@ -166,7 +182,7 @@ def authenticate_request(
166182 cookie : bytes | None ,
167183 authorization_header : bytes | None ,
168184 ip_address : AnyIPAddress ,
169- ) -> tuple [Participation | None , bytes | None ]:
185+ ) -> tuple [Participation | None , bytes | None , bool ]:
170186 """Authenticate a user returning to the site, with a cookie.
171187
172188 Given the information the user's browser provided (the cookie) and
@@ -200,16 +216,17 @@ def authenticate_request(
200216 timestamp: the date and the time of the request.
201217 cookie: the cookie the user's browser provided in the
202218 request (if any).
219+ authorization_header: the value of X-CMS-Authorization header (if any).
203220 ip_address: the IP address the request
204221 came from.
205222
206- return: if the user
207- couldn't be authenticated then return None, otherwise return
208- the participation that they wanted to authenticate as; if a
209- cookie has to be set return it as well, otherwise return None.
223+ return: a tuple consisting of participation (None if authentication failed),
224+ a cookie that has to be set (or None), and a boolean flag indicating
225+ whether the admin token was used to impersonate a user.
210226
211227 """
212228 participation : Participation | None = None
229+ impersonated = False
213230
214231 if contest .ip_autologin :
215232 try :
@@ -219,34 +236,37 @@ def authenticate_request(
219236 if participation is not None :
220237 cookie = None
221238 except AmbiguousIPAddress :
222- return None , None
239+ return None , None , False
223240
224- if participation is None \
225- and contest .allow_password_authentication :
226- participation , cookie = _authenticate_request_from_cookie_or_authorization_header (
227- sql_session , contest , timestamp , authorization_header if authorization_header is not None else cookie )
241+ if participation is None :
242+ participation , cookie , impersonated = (
243+ _authenticate_request_from_cookie_or_authorization_header (
244+ sql_session , contest , timestamp ,
245+ authorization_header if authorization_header is not None else cookie ))
228246
229247 if participation is None :
230- return None , None
248+ return None , None , False
231249
232250 # Check if user is using the right IP (or is on the right subnet).
233- if contest .ip_restriction and participation .ip is not None \
234- and not any (ip_address in network for network in participation .ip ):
251+ if (contest .ip_restriction and participation .ip is not None
252+ and not impersonated
253+ and not any (ip_address in network for network in participation .ip )):
235254 logger .info (
236255 "Unsuccessful authentication from IP address %s, on contest %s, "
237256 "as %s, at %s: unauthorized IP address" ,
238257 ip_address , contest .name , participation .user .username , timestamp )
239- return None , None
258+ return None , None , False
240259
241260 # Check that the user is not hidden if hidden users are blocked.
242- if contest .block_hidden_participations and participation .hidden :
261+ if (contest .block_hidden_participations and participation .hidden
262+ and not impersonated ):
243263 logger .info (
244264 "Unsuccessful authentication from IP address %s, on contest %s, "
245265 "as %s, at %s: participation is hidden and unauthorized" ,
246266 ip_address , contest .name , participation .user .username , timestamp )
247- return None , None
267+ return None , None , False
248268
249- return participation , cookie
269+ return participation , cookie , impersonated
250270
251271
252272def _authenticate_request_by_ip_address (
@@ -311,7 +331,7 @@ def _authenticate_request_by_ip_address(
311331
312332def _authenticate_request_from_cookie_or_authorization_header (
313333 sql_session : Session , contest : Contest , timestamp : datetime , cookie : bytes | None
314- ) -> tuple [Participation | None , bytes | None ]:
334+ ) -> tuple [Participation | None , bytes | None , bool ]:
315335 """Return the current participation based on the cookie.
316336
317337 If a participation can be extracted, the cookie is refreshed.
@@ -323,26 +343,31 @@ def _authenticate_request_from_cookie_or_authorization_header(
323343 cookie: the contents of the cookie (or authorization header)
324344 provided in the request (if any).
325345
326- return: the participation
327- extracted from the cookie and the cookie to set/refresh, or
328- None in case of errors .
346+ return: a triple of the participation extracted from the cookie (or None),
347+ the cookie to set/refresh (or None), and a boolean flag indicating
348+ impersonation of the user by the administrator .
329349
330350 """
331351 if cookie is None :
332352 logger .info ("Unsuccessful cookie authentication: no cookie provided" )
333- return None , None
353+ return None , None , False
334354
335355 # Parse cookie.
336356 try :
337- cookie = json .loads (cookie .decode ("utf-8" ))
357+ cookie : typing . Any = json .loads (cookie .decode ("utf-8" ))
338358 username : str = cookie [0 ]
339359 password : str = cookie [1 ]
340360 last_update = make_datetime (cookie [2 ])
361+ impersonated : bool = cookie [3 ]
341362 except Exception as e :
342363 # Cookies are stored securely and thus cannot be tampered with:
343364 # this is either a programming or a configuration error.
344365 logger .warning ("Invalid cookie (%s): %s" , e , cookie )
345- return None , None
366+ return None , None , False
367+
368+ # Reject if password authentication is disabled and it's not an impersonation cookie/header.
369+ if not contest .allow_password_authentication and not impersonated :
370+ return None , None , False
346371
347372 def log_failed_attempt (msg , * args ):
348373 logger .info ("Unsuccessful cookie authentication as %r, returning from "
@@ -353,7 +378,7 @@ def log_failed_attempt(msg, *args):
353378 if timestamp - last_update > timedelta (seconds = config .cookie_duration ):
354379 log_failed_attempt ("cookie expired (lasts %d seconds)" ,
355380 config .cookie_duration )
356- return None , None
381+ return None , None , False
357382
358383 # Load participation from DB and make sure it exists.
359384 participation : Participation | None = (
@@ -366,22 +391,28 @@ def log_failed_attempt(msg, *args):
366391 )
367392 if participation is None :
368393 log_failed_attempt ("user not registered to contest" )
369- return None , None
394+ return None , None , False
370395
371- correct_password = get_password (participation )
372-
373- # We compare hashed password because it would be too expensive to
374- # re-hash the user-provided plaintext password at every request.
375- if password != correct_password :
376- log_failed_attempt ("wrong password" )
377- return None , None
396+ if impersonated :
397+ correct_password = ""
398+ logger .info ("Successful impersonation of user %r, on contest %s, "
399+ "returning from %s, at %s" , username , contest .name , last_update ,
400+ timestamp )
401+ else :
402+ # We compare hashed password because it would be too expensive to
403+ # re-hash the user-provided plaintext password at every request.
404+ correct_password = get_password (participation )
405+ if password != correct_password :
406+ log_failed_attempt ("wrong password" )
407+ return None , None , False
378408
379- logger .info ("Successful cookie authentication as user %r, on contest %s, "
380- "returning from %s, at %s" , username , contest .name , last_update ,
381- timestamp )
409+ logger .info ("Successful cookie authentication as user %r, on contest %s, "
410+ "returning from %s, at %s" , username , contest .name , last_update ,
411+ timestamp )
382412
383413 # We store the hashed password (if hashing is used) so that the
384414 # expensive bcrypt hashing doesn't need to be done at every request.
385415 return (participation ,
386- json .dumps ([username , correct_password , make_timestamp (timestamp )])
387- .encode ("utf-8" ))
416+ json .dumps ([username , correct_password , make_timestamp (timestamp ), impersonated ])
417+ .encode ("utf-8" ),
418+ impersonated )
0 commit comments