@@ -50,7 +50,13 @@ def setUp(self):
5050 self .participation = self .add_participation (
5151 contest = self .contest , user = self .user )
5252
53- def assertSuccess (self , username , password , ip_address ):
53+ # Set up a temporary admin token
54+ patcher = patch .object (config , "contest_admin_token" , "admin-token" )
55+ self .addCleanup (patcher .stop )
56+ patcher .start ()
57+
58+
59+ def assertSuccess (self , username , password , ip_address , admin_token = "" ):
5460 # We had an issue where due to a misuse of contains_eager we ended up
5561 # with the wrong user attached to the participation. This only happens
5662 # if the correct user isn't already in the identity map, which is what
@@ -61,18 +67,20 @@ def assertSuccess(self, username, password, ip_address):
6167
6268 authenticated_participation , cookie = validate_login (
6369 self .session , self .contest , self .timestamp ,
64- username , password , ipaddress .ip_address (ip_address ))
70+ username , password , ipaddress .ip_address (ip_address ),
71+ admin_token )
6572
6673 self .assertIsNotNone (authenticated_participation )
6774 self .assertIsNotNone (cookie )
6875 self .assertIs (authenticated_participation , self .participation )
6976 self .assertIs (authenticated_participation .user , self .user )
7077 self .assertIs (authenticated_participation .contest , self .contest )
7178
72- def assertFailure (self , username , password , ip_address ):
79+ def assertFailure (self , username , password , ip_address , admin_token = "" ):
7380 authenticated_participation , cookie = validate_login (
7481 self .session , self .contest , self .timestamp ,
75- username , password , ipaddress .ip_address (ip_address ))
82+ username , password , ipaddress .ip_address (ip_address ),
83+ admin_token )
7684
7785 self .assertIsNone (authenticated_participation )
7886 self .assertIsNone (cookie )
@@ -150,6 +158,30 @@ def test_deactivated_ip_lock(self):
150158
151159 self .assertSuccess ("myuser" , "mypass" , "10.0.1.1" )
152160
161+ def test_successful_impersonation (self ):
162+ self .assertSuccess ("myuser" , "" , "127.0.0.1" , "admin-token" )
163+
164+ def test_unsuccessful_impersonation (self ):
165+ self .assertFailure ("myuser" , "" , "127.0.0.1" , "bad-admin-token" )
166+
167+ def test_impersonation_overrides_unallowed_password_authentication (self ):
168+ self .contest .allow_password_authentication = False
169+
170+ self .assertSuccess ("myuser" , "" , "127.0.0.1" , "admin-token" )
171+
172+ def test_impersonation_overrides_unallowed_hidden_participation (self ):
173+ self .contest .block_hidden_participations = True
174+ self .participation .hidden = True
175+
176+ self .assertSuccess ("myuser" , "" , "127.0.0.1" , "admin-token" )
177+
178+ def test_impersonation_overrides_ip_lock (self ):
179+ self .contest .ip_restriction = True
180+ self .participation .ip = [ipaddress .ip_network ("10.0.0.0/24" )]
181+
182+ self .assertSuccess ("myuser" , "mypass" , "10.0.0.1" , "admin-token" )
183+ self .assertSuccess ("myuser" , "mypass" , "10.0.1.1" , "admin-token" )
184+
153185
154186class TestAuthenticateRequest (DatabaseMixin , unittest .TestCase ):
155187
@@ -158,7 +190,6 @@ def setUp(self):
158190 self .timestamp = make_datetime ()
159191 self .add_contest ()
160192 self .contest = self .add_contest ()
161- self .add_user (username = "otheruser" )
162193 self .user = self .add_user (
163194 username = "myuser" , password = build_password ("mypass" ))
164195 self .participation = self .add_participation (
@@ -167,7 +198,26 @@ def setUp(self):
167198 self .session , self .contest , self .timestamp , self .user .username ,
168199 "mypass" , ipaddress .ip_address ("10.0.0.1" ))
169200
170- def attempt_authentication (self , ** kwargs ):
201+ # For testing impersonation by admin token
202+ self .impersonated_user = self .add_user (username = "otheruser" )
203+ self .impersonated_participation = self .add_participation (
204+ contest = self .contest , user = self .impersonated_user )
205+ with patch .object (config , "contest_admin_token" , "admin-token" ):
206+ _ , self .impersonated_cookie = validate_login (
207+ self .session , self .contest , self .timestamp , "otheruser" ,
208+ "" , ipaddress .ip_address ("10.0.0.2" ), "admin-token" )
209+
210+ def attempt_authentication (self , db_flush = True , ** kwargs ):
211+ # We had an issue where due to a misuse of contains_eager we ended up
212+ # with the wrong user attached to the participation. This only happens
213+ # if the correct user isn't already in the identity map, which is what
214+ # these lines trigger.
215+ if db_flush :
216+ self .session .flush ()
217+ self .session .expire (self .user )
218+ self .session .expire (self .impersonated_user )
219+ self .session .expire (self .contest )
220+
171221 # The arguments need to be passed as keywords and are timestamp, cookie
172222 # and ip_address. A missing argument means the default value is used
173223 # instead. An argument passed as None means that None will be used.
@@ -179,19 +229,6 @@ def attempt_authentication(self, **kwargs):
179229 ipaddress .ip_address (kwargs .get ("ip_address" , "10.0.0.1" )))
180230
181231 def assertSuccess (self , ** kwargs ):
182- # Assert that the authentication succeeds in any way (be it through IP
183- # autologin or thanks to the cookie) and return the cookie that should
184- # be set (or None, if it should be cleared/left unset).
185- # The arguments are the same as those of attempt_authentication.
186-
187- # We had an issue where due to a misuse of contains_eager we ended up
188- # with the wrong user attached to the participation. This only happens
189- # if the correct user isn't already in the identity map, which is what
190- # these lines trigger.
191- self .session .flush ()
192- self .session .expire (self .user )
193- self .session .expire (self .contest )
194-
195232 authenticated_participation , cookie , impersonated = \
196233 self .attempt_authentication (** kwargs )
197234
@@ -221,6 +258,21 @@ def assertSuccessAndCookieCleared(self, **kwargs):
221258 cookie = self .assertSuccess (** kwargs )
222259 self .assertIsNone (cookie )
223260
261+ def assertImpersonationSuccess (self , ** kwargs ):
262+ # Assert that the impersonation succeeds.
263+ # The arguments are the same as those of attempt_authentication.
264+
265+ authenticated_participation , cookie , impersonated = \
266+ self .attempt_authentication (cookie = self .impersonated_cookie , ** kwargs )
267+
268+ self .assertIsNotNone (authenticated_participation )
269+ self .assertIs (authenticated_participation , self .impersonated_participation )
270+ self .assertIs (authenticated_participation .user , self .impersonated_user )
271+ self .assertIs (authenticated_participation .contest , self .contest )
272+ self .assertIs (impersonated , True )
273+
274+ return cookie
275+
224276 def assertFailure (self , ** kwargs ):
225277 # Assert that the authentication fails.
226278 # The arguments are the same as those of attempt_authentication.
@@ -337,7 +389,7 @@ def test_authorization_header(self):
337389
338390 def test_no_user (self ):
339391 self .session .delete (self .user )
340- self .assertFailure ()
392+ self .assertFailure (db_flush = False )
341393
342394 def test_no_participation_for_user_in_contest (self ):
343395 self .session .delete (self .participation )
@@ -372,6 +424,30 @@ def test_ip_lock(self):
372424 self .participation .ip = None
373425 self .assertSuccessAndCookieRefreshed ()
374426
427+ def test_impersonate (self ):
428+ self .contest .ip_autologin = False
429+ self .contest .allow_password_authentication = False
430+ self .assertImpersonationSuccess ()
431+
432+ def test_impersonate_overridden_by_ip_autologin (self ):
433+ self .contest .ip_autologin = True
434+ self .contest .allow_password_authentication = False
435+
436+ self .participation .ip = [ipaddress .ip_network ("10.0.0.1/32" )]
437+ self .assertSuccessAndCookieCleared (cookie = self .impersonated_cookie )
438+
439+ def test_impersonation_overrides_unallowed_hidden_participation (self ):
440+ self .contest .block_hidden_participations = True
441+ self .participation .hidden = True
442+ self .assertImpersonationSuccess ()
443+
444+ def test_impersonation_overrides_ip_lock (self ):
445+ self .contest .ip_restriction = True
446+ self .participation .ip = [ipaddress .ip_network ("10.0.0.0/24" )]
447+
448+ self .assertImpersonationSuccess (ip_address = "10.0.0.1" )
449+ self .assertImpersonationSuccess (ip_address = "10.0.1.1" )
450+
375451
376452if __name__ == "__main__" :
377453 unittest .main ()
0 commit comments