Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to persist cookies #1720

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Comment on lines -471 to +498
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better if we can support both tuple and dict, with a deprecation warning for the tuple. As we need to consider all users

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()
Loading