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