diff --git a/jira/client.py b/jira/client.py index 34651292f..74404ea4a 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,7 +338,8 @@ 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, kerberos=False, @@ -347,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. @@ -369,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 @@ -466,6 +483,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 +3431,20 @@ 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: Optional[Union[Union[float, int], Tuple[float, float]]], + ): + """ + Creates token-based session. + Header structure: "authorization": "Bearer " + """ + verify = self._options["verify"] + self._session = ResilientSession(timeout=timeout) + self._session.verify = verify + self._session.auth = TokenAuth(token_auth) + def _set_avatar(self, params, url, avatar): data = {"id": avatar} return self._session.put(url, params=params, data=json.dumps(data)) 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()