Skip to content

Commit 1176bfd

Browse files
committedOct 8, 2024
refactor 401 retrying into both auth methods
1 parent 9900396 commit 1176bfd

File tree

1 file changed

+80
-42
lines changed

1 file changed

+80
-42
lines changed
 

‎jira/client.py

+80-42
Original file line numberDiff line numberDiff line change
@@ -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

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

Comments
 (0)