Skip to content

Commit f4aa2bd

Browse files
authored
restore cookie auth (#1201)
* fix cookie auth * add a test * fix RecursionError * add a log message so we know what's going on
1 parent 4a80c85 commit f4aa2bd

File tree

2 files changed

+92
-32
lines changed

2 files changed

+92
-32
lines changed

jira/client.py

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -210,59 +210,76 @@ class JiraCookieAuth(AuthBase):
210210
"""Jira Cookie Authentication
211211
212212
Allows using cookie authentication as described by
213-
https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-cookie-based-authentication
214-
213+
https://developer.atlassian.com/server/jira/platform/cookie-based-authentication/
215214
"""
216215

217216
def __init__(
218-
self, session: ResilientSession, _get_session: Callable, auth: Tuple[str, str]
217+
self, session: ResilientSession, session_api_url: str, auth: Tuple[str, str]
219218
):
220219
"""Cookie Based Authentication
221220
222221
Args:
223222
session (ResilientSession): The Session object to communicate with the API.
224-
_get_session (Callable): The function that returns a :py_class:``User``
225-
auth (Tuple[str, str]): The username, password tuple
223+
session_api_url (str): The session api url to use.
224+
auth (Tuple[str, str]): The username, password tuple.
226225
"""
226+
227227
self._session = session
228-
self._get_session = _get_session
228+
self._session_api_url = session_api_url # e.g ."/rest/auth/1/session"
229229
self.__auth = auth
230+
self._retry_counter_401 = 0
231+
self._max_allowed_401_retries = 1 # 401 aren't recoverable with retries really
232+
233+
@property
234+
def cookies(self):
235+
return self._session.cookies
236+
237+
def _increment_401_retry_counter(self):
238+
self._retry_counter_401 += 1
239+
240+
def _reset_401_retry_counter(self):
241+
self._retry_counter_401 = 0
230242

231-
def handle_401(self, response, **kwargs):
232-
if response.status_code != 401:
233-
return response
234-
self.init_session()
235-
response = self.process_original_request(response.request.copy())
243+
def __call__(self, request: requests.PreparedRequest):
244+
request.register_hook("response", self.handle_401)
245+
return request
246+
247+
def init_session(self):
248+
"""Initialise the Session object's cookies, so we can use the session cookie."""
249+
username, password = self.__auth
250+
authentication_data = {"username": username, "password": password}
251+
r = self._session.post( # this also goes through the handle_401() hook
252+
self._session_api_url, data=json.dumps(authentication_data)
253+
)
254+
r.raise_for_status()
255+
256+
def handle_401(self, response: requests.Response, **kwargs):
257+
"""Refresh cookies if the session cookie has expired. Then retry the request."""
258+
if (
259+
response.status_code == 401
260+
and self._retry_counter_401 < self._max_allowed_401_retries
261+
):
262+
LOG.info("Trying to refresh the cookie auth session...")
263+
self._increment_401_retry_counter()
264+
self.init_session()
265+
response = self.process_original_request(response.request.copy())
266+
self._reset_401_retry_counter()
236267
return response
237268

238-
def process_original_request(self, original_request):
269+
def process_original_request(self, original_request: requests.PreparedRequest):
239270
self.update_cookies(original_request)
240271
return self.send_request(original_request)
241272

242-
def update_cookies(self, original_request):
273+
def update_cookies(self, original_request: requests.PreparedRequest):
243274
# Cookie header needs first to be deleted for the header to be updated using
244275
# the prepare_cookies method. See request.PrepareRequest.prepare_cookies
245276
if "Cookie" in original_request.headers:
246277
del original_request.headers["Cookie"]
247278
original_request.prepare_cookies(self.cookies)
248279

249-
def init_session(self):
250-
self.start_session()
251-
252-
def __call__(self, request):
253-
request.register_hook("response", self.handle_401)
254-
return request
255-
256-
def send_request(self, request):
280+
def send_request(self, request: requests.PreparedRequest):
257281
return self._session.send(request)
258282

259-
@property
260-
def cookies(self):
261-
return self._session.cookies
262-
263-
def start_session(self):
264-
self._get_session(self.__auth)
265-
266283

267284
class TokenAuth(AuthBase):
268285
"""Bearer Token Authentication"""
@@ -571,8 +588,17 @@ def _create_cookie_auth(
571588
auth: Tuple[str, str],
572589
timeout: Optional[Union[Union[float, int], Tuple[float, float]]],
573590
):
591+
warnings.warn(
592+
"Use OAuth or Token based authentication "
593+
+ "instead of Cookie based Authentication.",
594+
DeprecationWarning,
595+
)
574596
self._session = ResilientSession(timeout=timeout)
575-
self._session.auth = JiraCookieAuth(self._session, self.session, auth)
597+
self._session.auth = JiraCookieAuth(
598+
session=self._session,
599+
session_api_url="{server}{auth_url}".format(**self._options),
600+
auth=auth,
601+
)
576602

577603
def _check_update_(self):
578604
"""Check if the current version of the library is outdated."""

tests/test_client.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import getpass
2+
from unittest import mock
23

34
import pytest
45

56
import jira.client
67
from jira.exceptions import JIRAError
78
from tests.conftest import JiraTestManager, get_unique_project_name
89

9-
# from tenacity import retry
10-
# from tenacity import wait_incrementing
11-
1210

1311
@pytest.fixture()
1412
def prep():
@@ -215,3 +213,39 @@ def test_token_auth(cl_admin: jira.client.JIRA):
215213
# THEN: The reported authenticated user of the token
216214
# matches the original token creator user.
217215
assert cl_admin.myself() == new_jira_client.myself()
216+
217+
218+
def test_cookie_auth(test_manager: JiraTestManager):
219+
"""Test Cookie based authentication works.
220+
221+
NOTE: this is deprecated in Cloud and is not recommended in Server.
222+
https://developer.atlassian.com/cloud/jira/platform/jira-rest-api-cookie-based-authentication/
223+
https://developer.atlassian.com/server/jira/platform/cookie-based-authentication/
224+
"""
225+
# GIVEN: the username and password
226+
# WHEN: We create a session with cookie auth for the same server
227+
cookie_auth_jira = jira.client.JIRA(
228+
server=test_manager.CI_JIRA_URL,
229+
auth=(test_manager.CI_JIRA_ADMIN, test_manager.CI_JIRA_ADMIN_PASSWORD),
230+
)
231+
# THEN: We get the same result from the API
232+
assert test_manager.jira_admin.myself() == cookie_auth_jira.myself()
233+
234+
235+
def test_cookie_auth_retry():
236+
"""Test Cookie based authentication retry logic works."""
237+
# GIVEN: arguments that will cause a 401 error
238+
auth_class = jira.client.JiraCookieAuth
239+
reset_func = jira.client.JiraCookieAuth._reset_401_retry_counter
240+
new_options = jira.client.JIRA.DEFAULT_OPTIONS.copy()
241+
new_options["auth_url"] = "/401"
242+
with pytest.raises(JIRAError):
243+
with mock.patch.object(auth_class, reset_func.__name__) as mock_reset_func:
244+
# WHEN: We create a session with cookie auth
245+
jira.client.JIRA(
246+
server="https://httpstat.us",
247+
options=new_options,
248+
auth=("user", "pass"),
249+
)
250+
# THEN: We don't get a RecursionError and only call the reset_function once
251+
mock_reset_func.assert_called_once()

0 commit comments

Comments
 (0)