@@ -310,30 +310,24 @@ def _sort_and_quote_values(self, values):
310310 return [quote (value , safe = "~" ) for value in ordered_values ]
311311
312312
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."""
318315
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 ):
329317 self ._session = session
330- self ._session_api_url = session_api_url # e.g ."/rest/auth/1/session"
331- self .__auth = auth
332318 self ._retry_counter_401 = 0
333319 self ._max_allowed_401_retries = 1 # 401 aren't recoverable with retries really
334320
321+ def init_session (self ):
322+ """Auth mechanism specific code to re-initialize the Jira session."""
323+ raise NotImplementedError ()
324+
335325 @property
336326 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
337331 return self ._session .cookies
338332
339333 def _increment_401_retry_counter (self ):
@@ -342,22 +336,6 @@ def _increment_401_retry_counter(self):
342336 def _reset_401_retry_counter (self ):
343337 self ._retry_counter_401 = 0
344338
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-
361339 def handle_401 (self , response : requests .Response , ** kwargs ) -> requests .Response :
362340 """Refresh cookies if the session cookie has expired. Then retry the request.
363341
@@ -367,36 +345,87 @@ def handle_401(self, response: requests.Response, **kwargs) -> requests.Response
367345 Returns:
368346 requests.Response
369347 """
370- if (
348+ is_retryable_401 = (
371349 response .status_code == 401
372350 and self ._retry_counter_401 < self ._max_allowed_401_retries
373- ):
351+ )
352+
353+ if is_retryable_401 and self ._session is not None :
374354 LOG .info ("Trying to refresh the cookie auth session..." )
375355 self ._increment_401_retry_counter ()
376356 self .init_session ()
377357 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+
378361 self ._reset_401_retry_counter ()
379362 return response
380363
381364 def process_original_request (self , original_request : requests .PreparedRequest ):
382365 self .update_cookies (original_request )
383366 return self .send_request (original_request )
384367
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+
385417 def update_cookies (self , original_request : requests .PreparedRequest ):
386418 # Cookie header needs first to be deleted for the header to be updated using the
387419 # prepare_cookies method. See request.PrepareRequest.prepare_cookies
388420 if "Cookie" in original_request .headers :
389421 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 )
394422
395423
396- class TokenAuth (AuthBase ):
424+ class TokenAuth (RetryingJiraAuth ):
397425 """Bearer Token Authentication."""
398426
399- def __init__ (self , token : str ):
427+ def __init__ (self , token : str , session : ResilientSession | None = None ):
428+ super ().__init__ (session )
400429 # setup any auth-related data here
401430 self ._token = token
402431
@@ -405,6 +434,15 @@ def __call__(self, r: requests.PreparedRequest):
405434 r .headers ["authorization" ] = f"Bearer { self ._token } "
406435 return r
407436
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+
408446
409447class JIRA :
410448 """User interface to Jira.
@@ -4499,7 +4537,7 @@ def _create_token_session(self, token_auth: str):
44994537
45004538 Header structure: "authorization": "Bearer <token_auth>".
45014539 """
4502- self ._session .auth = TokenAuth (token_auth )
4540+ self ._session .auth = TokenAuth (token_auth , session = self . _session )
45034541
45044542 def _set_avatar (self , params , url , avatar ):
45054543 data = {"id" : avatar }
0 commit comments