@@ -307,30 +307,24 @@ def _sort_and_quote_values(self, values):
307
307
return [quote (value , safe = "~" ) for value in ordered_values ]
308
308
309
309
310
- class JiraCookieAuth (AuthBase ):
311
- """Jira Cookie Authentication.
312
-
313
- Allows using cookie authentication as described by `jira api docs <https://developer.atlassian.com/server/jira/platform/cookie-based-authentication/>`_
314
- """
310
+ class RetryingJiraAuth (AuthBase ):
311
+ """Base class for Jira authentication handlers that need to retry requests on 401 responses."""
315
312
316
- def __init__ (
317
- self , session : ResilientSession , session_api_url : str , auth : tuple [str , str ]
318
- ):
319
- """Cookie Based Authentication.
320
-
321
- Args:
322
- session (ResilientSession): The Session object to communicate with the API.
323
- session_api_url (str): The session api url to use.
324
- auth (Tuple[str, str]): The username, password tuple.
325
- """
313
+ def __init__ (self , session : ResilientSession | None = None ):
326
314
self ._session = session
327
- self ._session_api_url = session_api_url # e.g ."/rest/auth/1/session"
328
- self .__auth = auth
329
315
self ._retry_counter_401 = 0
330
316
self ._max_allowed_401_retries = 1 # 401 aren't recoverable with retries really
331
317
318
+ def init_session (self ):
319
+ """Auth mechanism specific code to re-initialize the Jira session."""
320
+ raise NotImplementedError ()
321
+
332
322
@property
333
323
def cookies (self ):
324
+ """Return the cookies from the session."""
325
+ assert (
326
+ self ._session is not None
327
+ ) # handle_401 should've caught this before attempting retry
334
328
return self ._session .cookies
335
329
336
330
def _increment_401_retry_counter (self ):
@@ -339,22 +333,6 @@ def _increment_401_retry_counter(self):
339
333
def _reset_401_retry_counter (self ):
340
334
self ._retry_counter_401 = 0
341
335
342
- def __call__ (self , request : requests .PreparedRequest ):
343
- request .register_hook ("response" , self .handle_401 )
344
- return request
345
-
346
- def init_session (self ):
347
- """Initialise the Session object's cookies, so we can use the session cookie.
348
-
349
- Raises HTTPError if the post returns an erroring http response
350
- """
351
- username , password = self .__auth
352
- authentication_data = {"username" : username , "password" : password }
353
- r = self ._session .post ( # this also goes through the handle_401() hook
354
- self ._session_api_url , data = json .dumps (authentication_data )
355
- )
356
- r .raise_for_status ()
357
-
358
336
def handle_401 (self , response : requests .Response , ** kwargs ) -> requests .Response :
359
337
"""Refresh cookies if the session cookie has expired. Then retry the request.
360
338
@@ -364,36 +342,87 @@ def handle_401(self, response: requests.Response, **kwargs) -> requests.Response
364
342
Returns:
365
343
requests.Response
366
344
"""
367
- if (
345
+ is_retryable_401 = (
368
346
response .status_code == 401
369
347
and self ._retry_counter_401 < self ._max_allowed_401_retries
370
- ):
348
+ )
349
+
350
+ if is_retryable_401 and self ._session is not None :
371
351
LOG .info ("Trying to refresh the cookie auth session..." )
372
352
self ._increment_401_retry_counter ()
373
353
self .init_session ()
374
354
response = self .process_original_request (response .request .copy ())
355
+ elif is_retryable_401 and self ._session is None :
356
+ LOG .warning ("No session was passed to constructor, can't refresh cookies." )
357
+
375
358
self ._reset_401_retry_counter ()
376
359
return response
377
360
378
361
def process_original_request (self , original_request : requests .PreparedRequest ):
379
362
self .update_cookies (original_request )
380
363
return self .send_request (original_request )
381
364
365
+ def update_cookies (self , original_request : requests .PreparedRequest ):
366
+ """Auth mechanism specific cookie handling prior to retrying."""
367
+ raise NotImplementedError ()
368
+
369
+ def send_request (self , request : requests .PreparedRequest ):
370
+ if self ._session is not None :
371
+ request .prepare_cookies (self .cookies ) # post-update re-prepare
372
+ return self ._session .send (request )
373
+
374
+
375
+ class JiraCookieAuth (RetryingJiraAuth ):
376
+ """Jira Cookie Authentication.
377
+
378
+ Allows using cookie authentication as described by `jira api docs <https://developer.atlassian.com/server/jira/platform/cookie-based-authentication/>`_
379
+ """
380
+
381
+ def __init__ (
382
+ self , session : ResilientSession , session_api_url : str , auth : tuple [str , str ]
383
+ ):
384
+ """Cookie Based Authentication.
385
+
386
+ Args:
387
+ session (ResilientSession): The Session object to communicate with the API.
388
+ session_api_url (str): The session api url to use.
389
+ auth (Tuple[str, str]): The username, password tuple.
390
+ """
391
+ super ().__init__ (session )
392
+ self ._session_api_url = session_api_url # e.g ."/rest/auth/1/session"
393
+ self .__auth = auth
394
+
395
+ def __call__ (self , request : requests .PreparedRequest ):
396
+ request .register_hook ("response" , self .handle_401 )
397
+ return request
398
+
399
+ def init_session (self ):
400
+ """Initialise the Session object's cookies, so we can use the session cookie.
401
+
402
+ Raises HTTPError if the post returns an erroring http response
403
+ """
404
+ assert (
405
+ self ._session is not None
406
+ ) # Constructor for this subclass always takes a session
407
+ username , password = self .__auth
408
+ authentication_data = {"username" : username , "password" : password }
409
+ r = self ._session .post ( # this also goes through the handle_401() hook
410
+ self ._session_api_url , data = json .dumps (authentication_data )
411
+ )
412
+ r .raise_for_status ()
413
+
382
414
def update_cookies (self , original_request : requests .PreparedRequest ):
383
415
# Cookie header needs first to be deleted for the header to be updated using the
384
416
# prepare_cookies method. See request.PrepareRequest.prepare_cookies
385
417
if "Cookie" in original_request .headers :
386
418
del original_request .headers ["Cookie" ]
387
- original_request .prepare_cookies (self .cookies )
388
-
389
- def send_request (self , request : requests .PreparedRequest ):
390
- return self ._session .send (request )
391
419
392
420
393
- class TokenAuth (AuthBase ):
421
+ class TokenAuth (RetryingJiraAuth ):
394
422
"""Bearer Token Authentication."""
395
423
396
- def __init__ (self , token : str ):
424
+ def __init__ (self , token : str , session : ResilientSession | None = None ):
425
+ super ().__init__ (session )
397
426
# setup any auth-related data here
398
427
self ._token = token
399
428
@@ -402,6 +431,15 @@ def __call__(self, r: requests.PreparedRequest):
402
431
r .headers ["authorization" ] = f"Bearer { self ._token } "
403
432
return r
404
433
434
+ def init_session (self ):
435
+ pass # token should still work, only thing needed is to clear session cookies which happens next
436
+
437
+ def update_cookies (self , _ ):
438
+ assert (
439
+ self ._session is not None
440
+ ) # handle_401 on the superclass should've caught this before attempting retry
441
+ self ._session .cookies .clear_session_cookies ()
442
+
405
443
406
444
class JIRA :
407
445
"""User interface to Jira.
@@ -4306,7 +4344,7 @@ def _create_token_session(self, token_auth: str):
4306
4344
4307
4345
Header structure: "authorization": "Bearer <token_auth>".
4308
4346
"""
4309
- self ._session .auth = TokenAuth (token_auth )
4347
+ self ._session .auth = TokenAuth (token_auth , session = self . _session )
4310
4348
4311
4349
def _set_avatar (self , params , url , avatar ):
4312
4350
data = {"id" : avatar }
0 commit comments