@@ -307,30 +307,24 @@ def _sort_and_quote_values(self, values):
307307 return [quote (value , safe = "~" ) for value in ordered_values ]
308308
309309
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."""
315312
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 ):
326314 self ._session = session
327- self ._session_api_url = session_api_url # e.g ."/rest/auth/1/session"
328- self .__auth = auth
329315 self ._retry_counter_401 = 0
330316 self ._max_allowed_401_retries = 1 # 401 aren't recoverable with retries really
331317
318+ def init_session (self ):
319+ """Auth mechanism specific code to re-initialize the Jira session."""
320+ raise NotImplementedError ()
321+
332322 @property
333323 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
334328 return self ._session .cookies
335329
336330 def _increment_401_retry_counter (self ):
@@ -339,22 +333,6 @@ def _increment_401_retry_counter(self):
339333 def _reset_401_retry_counter (self ):
340334 self ._retry_counter_401 = 0
341335
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-
358336 def handle_401 (self , response : requests .Response , ** kwargs ) -> requests .Response :
359337 """Refresh cookies if the session cookie has expired. Then retry the request.
360338
@@ -364,36 +342,87 @@ def handle_401(self, response: requests.Response, **kwargs) -> requests.Response
364342 Returns:
365343 requests.Response
366344 """
367- if (
345+ is_retryable_401 = (
368346 response .status_code == 401
369347 and self ._retry_counter_401 < self ._max_allowed_401_retries
370- ):
348+ )
349+
350+ if is_retryable_401 and self ._session is not None :
371351 LOG .info ("Trying to refresh the cookie auth session..." )
372352 self ._increment_401_retry_counter ()
373353 self .init_session ()
374354 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+
375358 self ._reset_401_retry_counter ()
376359 return response
377360
378361 def process_original_request (self , original_request : requests .PreparedRequest ):
379362 self .update_cookies (original_request )
380363 return self .send_request (original_request )
381364
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+
382414 def update_cookies (self , original_request : requests .PreparedRequest ):
383415 # Cookie header needs first to be deleted for the header to be updated using the
384416 # prepare_cookies method. See request.PrepareRequest.prepare_cookies
385417 if "Cookie" in original_request .headers :
386418 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 )
391419
392420
393- class TokenAuth (AuthBase ):
421+ class TokenAuth (RetryingJiraAuth ):
394422 """Bearer Token Authentication."""
395423
396- def __init__ (self , token : str ):
424+ def __init__ (self , token : str , session : ResilientSession | None = None ):
425+ super ().__init__ (session )
397426 # setup any auth-related data here
398427 self ._token = token
399428
@@ -402,6 +431,15 @@ def __call__(self, r: requests.PreparedRequest):
402431 r .headers ["authorization" ] = f"Bearer { self ._token } "
403432 return r
404433
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+
405443
406444class JIRA :
407445 """User interface to Jira.
@@ -4306,7 +4344,7 @@ def _create_token_session(self, token_auth: str):
43064344
43074345 Header structure: "authorization": "Bearer <token_auth>".
43084346 """
4309- self ._session .auth = TokenAuth (token_auth )
4347+ self ._session .auth = TokenAuth (token_auth , session = self . _session )
43104348
43114349 def _set_avatar (self , params , url , avatar ):
43124350 data = {"id" : avatar }
0 commit comments