Skip to content

Commit

Permalink
Add option to persist cookies
Browse files Browse the repository at this point in the history
* Change `auth` param from tuple to dict to support optional
  `cookie_jar` param.
* Add option to provide callable for lazy password fetching.
  • Loading branch information
dosy4ev committed Aug 20, 2023
1 parent da00ec0 commit eb3a752
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 8 deletions.
39 changes: 33 additions & 6 deletions jira/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from collections import OrderedDict
from collections.abc import Iterable
from functools import lru_cache, wraps
from http.cookiejar import FileCookieJar, MozillaCookieJar
from io import BufferedReader
from numbers import Number
from typing import (
Expand Down Expand Up @@ -234,21 +235,34 @@ class JiraCookieAuth(AuthBase):
"""

def __init__(
self, session: ResilientSession, session_api_url: str, auth: tuple[str, str]
self,
session: ResilientSession,
session_api_url: str,
auth: dict[str, Any],
):
"""Cookie Based Authentication.
Args:
session (ResilientSession): The Session object to communicate with the API.
session_api_url (str): The session api url to use.
auth (Tuple[str, str]): The username, password tuple.
auth (Dict[str, Any]): A dict of properties for Cookie authentication.
"""
self._session = session
self._session_api_url = session_api_url # e.g ."/rest/auth/1/session"
self.__auth = auth
self._retry_counter_401 = 0
self._max_allowed_401_retries = 1 # 401 aren't recoverable with retries really

cookie_jar = auth.get("cookie_jar")
if cookie_jar:
self._session.cookies = MozillaCookieJar(cookie_jar) # type: ignore

if isinstance(self._session.cookies, FileCookieJar):
try:
self._session.cookies.load(ignore_discard=True)
except OSError:
pass # it's ok if file is not present

@property
def cookies(self):
return self._session.cookies
Expand All @@ -269,12 +283,18 @@ def init_session(self):
Raises:
HTTPError: if the post returns an erroring http response
"""
username, password = self.__auth
username = self.__auth["username"]
password = self.__auth["password"]
if callable(password):
password = password()

authentication_data = {"username": username, "password": password}
r = self._session.post( # this also goes through the handle_401() hook
self._session_api_url, data=json.dumps(authentication_data)
)
r.raise_for_status()
if isinstance(self._session.cookies, FileCookieJar):
self._session.cookies.save(ignore_discard=True)

def handle_401(self, response: requests.Response, **kwargs) -> requests.Response:
"""Refresh cookies if the session cookie has expired. Then retry the request.
Expand Down Expand Up @@ -400,7 +420,7 @@ def __init__(
max_retries: int = 3,
proxies: Any = None,
timeout: None | float | tuple[float, float] | tuple[float, None] | None = None,
auth: tuple[str, str] = None,
auth: dict[str, Any] = None,
default_batch_sizes: dict[type[Resource], int | None] | None = None,
):
"""Construct a Jira client instance.
Expand Down Expand Up @@ -468,7 +488,14 @@ def __init__(
Obviously this means that you cannot rely on the return code when this is enabled.
max_retries (int): Sets the amount Retries for the HTTP sessions initiated by the client. (Default: ``3``)
proxies (Optional[Any]): Sets the proxies for the HTTP session.
auth (Optional[Tuple[str,str]]): Set a cookie auth token if this is required.
auth (Optional[Dict[str, Any]]): A dict of properties for Cookei authentication.
The following properties are available:
* username (srt) -- username
* password (str | Callable[[], str]) -- password or function for lazy receiving password
* cookie_jar (Optional[srt]) -- file path for persisting cookies
logging (bool): True enables loglevel to info => else critical. (Default: ``True``)
default_batch_sizes (Optional[Dict[Type[Resource], Optional[int]]]): Manually specify the batch-sizes for
the paginated retrieval of different item types. `Resource` is used as a fallback for every item type not
Expand Down Expand Up @@ -622,7 +649,7 @@ def _is_cloud(self) -> bool:
"""Return whether we are on a Cloud based Jira instance."""
return self.deploymentType in ("Cloud",)

def _create_cookie_auth(self, auth: tuple[str, str]):
def _create_cookie_auth(self, auth: dict[str, Any]):
warnings.warn(
"Use OAuth or Token based authentication "
+ "instead of Cookie based Authentication.",
Expand Down
7 changes: 5 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,10 @@ def test_cookie_auth(test_manager: JiraTestManager):
# WHEN: We create a session with cookie auth for the same server
cookie_auth_jira = jira.client.JIRA(
server=test_manager.CI_JIRA_URL,
auth=(test_manager.CI_JIRA_ADMIN, test_manager.CI_JIRA_ADMIN_PASSWORD),
auth={
"username": test_manager.CI_JIRA_ADMIN,
"password": test_manager.CI_JIRA_ADMIN_PASSWORD,
},
)
# THEN: We get the same result from the API
assert test_manager.jira_admin.myself() == cookie_auth_jira.myself()
Expand All @@ -248,7 +251,7 @@ def test_cookie_auth_retry():
jira.client.JIRA(
server="https://httpstat.us",
options=new_options,
auth=("user", "pass"),
auth={"username": "user", "password": "pass"},
)
# THEN: We don't get a RecursionError and only call the reset_function once
mock_reset_func.assert_called_once()

0 comments on commit eb3a752

Please sign in to comment.