Skip to content

Commit d07e748

Browse files
gmishkinstudioj
authored andcommitted
refactor 401 retrying into both auth methods
1 parent 7676b47 commit d07e748

File tree

1 file changed

+80
-42
lines changed

1 file changed

+80
-42
lines changed

jira/client.py

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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

409447
class 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

Comments
 (0)