From d105ba3ea8597dd098e898b7343353a2cfe08bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anna=20Przybycie=C5=84?= Date: Thu, 10 Dec 2020 13:42:36 +0100 Subject: [PATCH 1/7] Added quick token_auth to client --- jira/client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/jira/client.py b/jira/client.py index 34651292f..8a7cd46c3 100644 --- a/jira/client.py +++ b/jira/client.py @@ -328,6 +328,7 @@ def __init__( basic_auth: Union[None, Tuple[str, str]] = None, oauth: Dict[str, Any] = None, jwt: Dict[str, Any] = None, + token_auth: str =None, kerberos=False, kerberos_options: Dict[str, Any] = None, validate=False, @@ -466,6 +467,8 @@ def __init__( self._session.headers.update(self._options["headers"]) elif jwt: self._create_jwt_session(jwt, timeout) + elif token_auth: + self._create_token_session(token_auth, timeout) elif kerberos: self._create_kerberos_session(timeout, kerberos_options=kerberos_options) elif auth: @@ -3412,6 +3415,12 @@ def _create_jwt_session( self._session.verify = bool(self._options["verify"]) self._session.auth = jwt_auth + def _create_token_session(self, token_auth, timeout): + verify = self._options["verify"] + self._session = ResilientSession(timeout=timeout) + self._session.verify = verify + self._options["headers"]["authorization"] = "Bearer " + token_auth + def _set_avatar(self, params, url, avatar): data = {"id": avatar} return self._session.put(url, params=params, data=json.dumps(data)) From 506b98912af63170fe448814217163a729073f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anna=20Przybycie=C5=84?= Date: Thu, 10 Dec 2020 15:08:55 +0100 Subject: [PATCH 2/7] Create TokenAuth class and properly implement token authorization --- jira/client.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 8a7cd46c3..f6268ff75 100644 --- a/jira/client.py +++ b/jira/client.py @@ -3419,7 +3419,8 @@ def _create_token_session(self, token_auth, timeout): verify = self._options["verify"] self._session = ResilientSession(timeout=timeout) self._session.verify = verify - self._options["headers"]["authorization"] = "Bearer " + token_auth + token_auth = TokenAuth(token_auth) + self._session.auth = token_auth def _set_avatar(self, params, url, avatar): data = {"id": avatar} @@ -4818,3 +4819,14 @@ def __init__(self, options=None, basic_auth=None, oauth=None, async_=None): JIRA.__init__( self, options=options, basic_auth=basic_auth, oauth=oauth, async_=async_ ) + +class TokenAuth(AuthBase): + """Token Authentication""" + def __init__(self, token: str): + # setup any auth-related data here + self._token = token + + def __call__(self, r): + # modify and return the request + r.headers['authorization'] = "Bearer " + self._token + return r From c70daea67d98a41eac18f3f5d1bcc07b4dd7927d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anna=20Przybycie=C5=84?= Date: Thu, 10 Dec 2020 15:14:46 +0100 Subject: [PATCH 3/7] Update docs by adding description to constructor and method --- jira/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index f6268ff75..33110a700 100644 --- a/jira/client.py +++ b/jira/client.py @@ -397,6 +397,8 @@ def __init__( Example jwt structure: ``{'secret': SHARED_SECRET, 'payload': {'iss': PLUGIN_KEY}}`` + token_auth (str): A string containg the token necessary for (PAT) token authorization. + validate (bool): If true it will validate your credentials first. Remember that if you are accessing Jira as anonymous it will fail to instantiate. get_server_info (bool): If true it will fetch server version info first to determine if some API calls @@ -3415,7 +3417,11 @@ def _create_jwt_session( self._session.verify = bool(self._options["verify"]) self._session.auth = jwt_auth - def _create_token_session(self, token_auth, timeout): + def _create_token_session(self, token_auth: str, timeout): + """ + Creates token-based session. + Header structure: "authorization": "Bearer " + """ verify = self._options["verify"] self._session = ResilientSession(timeout=timeout) self._session.verify = verify From ceb0dd8ec29ee9da9269538a9b0e76885bac2ab9 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Sun, 24 Oct 2021 15:54:32 +0100 Subject: [PATCH 4/7] address lint also added contributions from another pull request Co-Authored-By: Alex Metzger --- jira/client.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/jira/client.py b/jira/client.py index 33110a700..fcb9e6a41 100644 --- a/jira/client.py +++ b/jira/client.py @@ -264,6 +264,19 @@ def start_session(self): self._get_session(self.__auth) +class TokenAuth(AuthBase): + """Bearer Token Authentication""" + + def __init__(self, token: str): + # setup any auth-related data here + self._token = token + + def __call__(self, r: requests.PreparedRequest): + # modify and return the request + r.headers["authorization"] = f"Bearer {self._token}" + return r + + class JIRA: """User interface to Jira. @@ -325,10 +338,10 @@ def __init__( self, server: str = None, options: Dict[str, Union[str, bool, Any]] = None, - basic_auth: Union[None, Tuple[str, str]] = None, + basic_auth: Optional[Tuple[str, str]] = None, + token_auth: Optional[str] = None, oauth: Dict[str, Any] = None, jwt: Dict[str, Any] = None, - token_auth: str =None, kerberos=False, kerberos_options: Dict[str, Any] = None, validate=False, @@ -348,8 +361,8 @@ def __init__( or ``atlas-run-standalone`` commands. By default, this instance runs at ``http://localhost:2990/jira``. The ``options`` argument can be used to set the Jira instance to use. - Authentication is handled with the ``basic_auth`` argument. If authentication is supplied (and is - accepted by Jira), the client will remember it for subsequent requests. + Authentication is handled with the ``basic_auth`` or ``token_auth`` argument. + If authentication is supplied (and is accepted by Jira), the client will remember it for subsequent requests. For quick command line access to a server, see the ``jirashell`` script included with this distribution. @@ -397,7 +410,7 @@ def __init__( Example jwt structure: ``{'secret': SHARED_SECRET, 'payload': {'iss': PLUGIN_KEY}}`` - token_auth (str): A string containg the token necessary for (PAT) token authorization. + token_auth (str): A string containing the token necessary for (PAT) bearer token authorization. validate (bool): If true it will validate your credentials first. Remember that if you are accessing Jira as anonymous it will fail to instantiate. @@ -3417,7 +3430,11 @@ def _create_jwt_session( self._session.verify = bool(self._options["verify"]) self._session.auth = jwt_auth - def _create_token_session(self, token_auth: str, timeout): + def _create_token_session( + self, + token_auth: str, + timeout: Optional[Union[Union[float, int], Tuple[float, float]]], + ): """ Creates token-based session. Header structure: "authorization": "Bearer " @@ -3425,8 +3442,7 @@ def _create_token_session(self, token_auth: str, timeout): verify = self._options["verify"] self._session = ResilientSession(timeout=timeout) self._session.verify = verify - token_auth = TokenAuth(token_auth) - self._session.auth = token_auth + self._session.auth = TokenAuth(token_auth) def _set_avatar(self, params, url, avatar): data = {"id": avatar} @@ -4826,13 +4842,3 @@ def __init__(self, options=None, basic_auth=None, oauth=None, async_=None): self, options=options, basic_auth=basic_auth, oauth=oauth, async_=async_ ) -class TokenAuth(AuthBase): - """Token Authentication""" - def __init__(self, token: str): - # setup any auth-related data here - self._token = token - - def __call__(self, r): - # modify and return the request - r.headers['authorization'] = "Bearer " + self._token - return r From 9be16782f07b910ca962f1c83958e5d6828216fb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:55:01 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- jira/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jira/client.py b/jira/client.py index fcb9e6a41..971cf8185 100644 --- a/jira/client.py +++ b/jira/client.py @@ -361,7 +361,7 @@ def __init__( or ``atlas-run-standalone`` commands. By default, this instance runs at ``http://localhost:2990/jira``. The ``options`` argument can be used to set the Jira instance to use. - Authentication is handled with the ``basic_auth`` or ``token_auth`` argument. + Authentication is handled with the ``basic_auth`` or ``token_auth`` argument. If authentication is supplied (and is accepted by Jira), the client will remember it for subsequent requests. For quick command line access to a server, see the ``jirashell`` script included with this distribution. @@ -4841,4 +4841,3 @@ def __init__(self, options=None, basic_auth=None, oauth=None, async_=None): JIRA.__init__( self, options=options, basic_auth=basic_auth, oauth=oauth, async_=async_ ) - From 8763367e424d6c939fd744cbf3f6e096177a7467 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Sun, 24 Oct 2021 15:59:43 +0100 Subject: [PATCH 6/7] move docstring around, update docstring typehint --- jira/client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jira/client.py b/jira/client.py index 971cf8185..74404ea4a 100644 --- a/jira/client.py +++ b/jira/client.py @@ -383,8 +383,11 @@ def __init__( * check_update -- Check whether using the newest python-jira library version. * headers -- a dict to update the default headers the session uses for all API requests. - basic_auth (Union[None, Tuple[str, str]]): A tuple of username and password to use when + basic_auth (Optional[Tuple[str, str]]): A tuple of username and password to use when establishing a session via HTTP BASIC authentication. + + token_auth (Optional[str]): A string containing the token necessary for (PAT) bearer token authorization. + oauth (Optional[Any]): A dict of properties for OAuth authentication. The following properties are required: * access_token -- OAuth access token for the user @@ -410,8 +413,6 @@ def __init__( Example jwt structure: ``{'secret': SHARED_SECRET, 'payload': {'iss': PLUGIN_KEY}}`` - token_auth (str): A string containing the token necessary for (PAT) bearer token authorization. - validate (bool): If true it will validate your credentials first. Remember that if you are accessing Jira as anonymous it will fail to instantiate. get_server_info (bool): If true it will fetch server version info first to determine if some API calls From 4ab7292a3ff689ec998dbbc35715273685ee1879 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Sun, 24 Oct 2021 18:00:57 +0100 Subject: [PATCH 7/7] add test for bearer token auth --- tests/test_client.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 3b8909e15..e29dea8ff 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,17 +16,17 @@ def prep(): @pytest.fixture(scope="module") -def test_manager(): +def test_manager() -> JiraTestManager: return JiraTestManager() @pytest.fixture() -def cl_admin(test_manager): +def cl_admin(test_manager: JiraTestManager) -> jira.client.JIRA: return test_manager.jira_admin @pytest.fixture() -def cl_normal(test_manager): +def cl_normal(test_manager: JiraTestManager) -> jira.client.JIRA: return test_manager.jira_normal @@ -194,3 +194,24 @@ def test_headers_unclobbered_update_with_no_provided_headers(no_fields): # THEN: we have not affected the other headers' defaults assert session_headers[invariant_header_name] == invariant_header_value + + +def test_token_auth(cl_admin: jira.client.JIRA): + """Tests the Personal Access Token authentication works.""" + # GIVEN: We have a PAT token created by a user. + pat_token_request = { + "name": "my_new_token", + "expirationDuration": 1, + } + base_url = cl_admin.server_url + pat_token_response = cl_admin._session.post( + f"{base_url}/rest/pat/latest/tokens", json=pat_token_request + ).json() + new_token = pat_token_response["rawToken"] + + # WHEN: A new client is authenticated with this token + new_jira_client = jira.client.JIRA(token_auth=new_token) + + # THEN: The reported authenticated user of the token + # matches the original token creator user. + assert cl_admin.myself() == new_jira_client.myself()