| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -84,14 +84,32 @@ Note on password authentication | |||
| 84 | 84 | ||
| 85 | 85 | GitLab has long removed password-based basic authentication. You can currently still use the | |
| 86 | 86 | `resource owner password credentials <https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow>`_ | |
| 87 | - flow to obtain an OAuth token. | ||
| 87 | + flow and python-gitlab will obtain an OAuth token for you when instantiated. | ||
| 88 | 88 | ||
| 89 | 89 | However, we do not recommend this as it will not work with 2FA enabled, and GitLab is removing | |
| 90 | - ROPC-based flows without client IDs in a future release. We recommend you obtain tokens for | ||
| 91 | - automated workflows as linked above or obtain a session cookie from your browser. | ||
| 90 | + ROPC-based flows without client credentials in a future release. We recommend you obtain tokens for | ||
| 91 | + automated workflows. | ||
| 92 | 92 | ||
| 93 | - For a python example of password authentication using the ROPC-based OAuth2 | ||
| 94 | - flow, see `this Ansible snippet <https://github.com/ansible-collections/community.general/blob/1c06e237c8100ac30d3941d5a3869a4428ba2974/plugins/module_utils/gitlab.py#L86-L92>`_. | ||
| 93 | + .. code-block:: python | ||
| 94 | + | ||
| 95 | + import gitlab | ||
| 96 | + from gitlab.oauth import PasswordCredentials | ||
| 97 | + | ||
| 98 | + oauth_credentials = PasswordCredentials("username", "password") | ||
| 99 | + gl = gitlab.Gitlab(oauth_credentials=oauth_credentials) | ||
| 100 | + | ||
| 101 | + # Define a specific OAuth scope | ||
| 102 | + oauth_credentials = PasswordCredentials("username", "password", scope="read_api") | ||
| 103 | + gl = gitlab.Gitlab(oauth_credentials=oauth_credentials) | ||
| 104 | + | ||
| 105 | + # Use with client credentials | ||
| 106 | + oauth_credentials = PasswordCredentials( | ||
| 107 | + "username", | ||
| 108 | + "password", | ||
| 109 | + client_id="your-client-id", | ||
| 110 | + client_secret="your-client-secret", | ||
| 111 | + ) | ||
| 112 | + gl = gitlab.Gitlab(oauth_credentials=oauth_credentials) | ||
| 95 | 113 | ||
| 96 | 114 | Managers | |
| 97 | 115 | ======== | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -168,8 +168,7 @@ We recommend that you use `Credential helpers`_ to securely store your tokens. | |||
| 168 | 168 | <https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html>`__ | |
| 169 | 169 | to learn how to obtain a token. | |
| 170 | 170 | * - ``oauth_token`` | |
| 171 | - - An Oauth token for authentication. The Gitlab server must be configured | ||
| 172 | - to support this authentication method. | ||
| 171 | + - An Oauth token for authentication. | ||
| 173 | 172 | * - ``job_token`` | |
| 174 | 173 | - Your job token. See `the official documentation | |
| 175 | 174 | <https://docs.gitlab.com/ce/api/jobs.html#get-job-artifacts>`__ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,7 +12,7 @@ | |||
| 12 | 12 | import gitlab.config | |
| 13 | 13 | import gitlab.const | |
| 14 | 14 | import gitlab.exceptions | |
| 15 | - from gitlab import _backends, utils | ||
| 15 | + from gitlab import _backends, oauth, utils | ||
| 16 | 16 | ||
| 17 | 17 | REDIRECT_MSG = ( | |
| 18 | 18 | "python-gitlab detected a {status_code} ({reason!r}) redirection. You must update " | |
@@ -41,8 +41,8 @@ class Gitlab: | |||
| 41 | 41 | the value is a string, it is the path to a CA file used for | |
| 42 | 42 | certificate validation. | |
| 43 | 43 | timeout: Timeout to use for requests to the GitLab server. | |
| 44 | - http_username: Username for HTTP authentication | ||
| 45 | - http_password: Password for HTTP authentication | ||
| 44 | + http_username: Username for OAuth ROPC flow (deprecated, use oauth_credentials) | ||
| 45 | + http_password: Password for OAuth ROPC flow (deprecated, use oauth_credentials) | ||
| 46 | 46 | api_version: Gitlab API version to use (support for 4 only) | |
| 47 | 47 | pagination: Can be set to 'keyset' to use keyset pagination | |
| 48 | 48 | order_by: Set order_by globally | |
@@ -51,6 +51,7 @@ class Gitlab: | |||
| 51 | 51 | or 52x responses. Defaults to False. | |
| 52 | 52 | keep_base_url: keep user-provided base URL for pagination if it | |
| 53 | 53 | differs from response headers | |
| 54 | + oauth_credentials: Password credentials for authenticating via OAuth ROPC flow | ||
| 54 | 55 | ||
| 55 | 56 | Keyword Args: | |
| 56 | 57 | requests.Session session: HTTP Requests Session | |
@@ -74,6 +75,8 @@ def __init__( | |||
| 74 | 75 | user_agent: str = gitlab.const.USER_AGENT, | |
| 75 | 76 | retry_transient_errors: bool = False, | |
| 76 | 77 | keep_base_url: bool = False, | |
| 78 | + *, | ||
| 79 | + oauth_credentials: Optional[oauth.PasswordCredentials] = None, | ||
| 77 | 80 | **kwargs: Any, | |
| 78 | 81 | ) -> None: | |
| 79 | 82 | self._api_version = str(api_version) | |
@@ -96,7 +99,7 @@ def __init__( | |||
| 96 | 99 | self.http_password = http_password | |
| 97 | 100 | self.oauth_token = oauth_token | |
| 98 | 101 | self.job_token = job_token | |
| 99 | - self._set_auth_info() | ||
| 102 | + self.oauth_credentials = oauth_credentials | ||
| 100 | 103 | ||
| 101 | 104 | #: Create a session object for requests | |
| 102 | 105 | _backend: Type[_backends.DefaultBackend] = kwargs.pop( | |
@@ -105,6 +108,7 @@ def __init__( | |||
| 105 | 108 | self._backend = _backend(**kwargs) | |
| 106 | 109 | self.session = self._backend.client | |
| 107 | 110 | ||
| 111 | + self._set_auth_info() | ||
| 108 | 112 | self.per_page = per_page | |
| 109 | 113 | self.pagination = pagination | |
| 110 | 114 | self.order_by = order_by | |
@@ -496,22 +500,56 @@ def _set_auth_info(self) -> None: | |||
| 496 | 500 | self.headers.pop("Authorization", None) | |
| 497 | 501 | self.headers["PRIVATE-TOKEN"] = self.private_token | |
| 498 | 502 | self.headers.pop("JOB-TOKEN", None) | |
| 503 | + return | ||
| 504 | + | ||
| 505 | + if not self.oauth_credentials and (self.http_username and self.http_password): | ||
| 506 | + utils.warn( | ||
| 507 | + "Passing http_username and http_password is deprecated and will be " | ||
| 508 | + "removed in a future version.\nPlease use the OAuth ROPC flow with" | ||
| 509 | + "(gitlab.oauth.PasswordCredentials) if you need password-based" | ||
| 510 | + "authentication. See https://docs.gitlab.com/ee/api/oauth2.html" | ||
| 511 | + "#resource-owner-password-credentials-flow for more details.", | ||
| 512 | + category=DeprecationWarning, | ||
| 513 | + ) | ||
| 514 | + self.oauth_credentials = oauth.PasswordCredentials( | ||
| 515 | + self.http_username, self.http_password | ||
| 516 | + ) | ||
| 517 | + | ||
| 518 | + if self.oauth_credentials: | ||
| 519 | + post_data = { | ||
| 520 | + "grant_type": self.oauth_credentials.grant_type, | ||
| 521 | + "scope": self.oauth_credentials.scope, | ||
| 522 | + "username": self.oauth_credentials.username, | ||
| 523 | + "password": self.oauth_credentials.password, | ||
| 524 | + } | ||
| 525 | + response = self.http_post( | ||
| 526 | + f"{self._base_url}/oauth/token", post_data=post_data | ||
| 527 | + ) | ||
| 528 | + if isinstance(response, dict): | ||
| 529 | + self.oauth_token = response["access_token"] | ||
| 530 | + else: | ||
| 531 | + self.oauth_token = response.json()["access_token"] | ||
| 532 | + self._http_auth = self.oauth_credentials.basic_auth | ||
| 499 | 533 | ||
| 500 | 534 | if self.oauth_token: | |
| 501 | 535 | self.headers["Authorization"] = f"Bearer {self.oauth_token}" | |
| 502 | 536 | self.headers.pop("PRIVATE-TOKEN", None) | |
| 503 | 537 | self.headers.pop("JOB-TOKEN", None) | |
| 538 | + return | ||
| 504 | 539 | ||
| 505 | 540 | if self.job_token: | |
| 506 | 541 | self.headers.pop("Authorization", None) | |
| 507 | 542 | self.headers.pop("PRIVATE-TOKEN", None) | |
| 508 | 543 | self.headers["JOB-TOKEN"] = self.job_token | |
| 509 | 544 | ||
| 545 | + <<<<<<< HEAD | ||
| 510 | 546 | if self.http_username and self.http_password: | |
| 511 | 547 | self._http_auth = requests.auth.HTTPBasicAuth( | |
| 512 | 548 | self.http_username, self.http_password | |
| 513 | 549 | ) | |
| 514 | 550 | ||
| 551 | + ======= | ||
| 552 | + >>>>>>> be7745dc (feat(client): replace basic auth with OAuth ROPC flow) | ||
| 515 | 553 | @staticmethod | |
| 516 | 554 | def enable_debug() -> None: | |
| 517 | 555 | import logging | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,33 @@ | |||
| 1 | + import dataclasses | ||
| 2 | + from typing import Optional | ||
| 3 | + | ||
| 4 | + | ||
| 5 | + @dataclasses.dataclass | ||
| 6 | + class PasswordCredentials: | ||
| 7 | + """ | ||
| 8 | + Resource owner password credentials modelled according to | ||
| 9 | + https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow | ||
| 10 | + https://datatracker.ietf.org/doc/html/rfc6749#section-4-3. | ||
| 11 | + | ||
| 12 | + If the GitLab server has disabled the ROPC flow without client credentials, | ||
| 13 | + client_id and client_secret must be provided. | ||
| 14 | + """ | ||
| 15 | + | ||
| 16 | + username: str | ||
| 17 | + password: str | ||
| 18 | + grant_type: str = "password" | ||
| 19 | + scope: str = "api" | ||
| 20 | + client_id: Optional[str] = None | ||
| 21 | + client_secret: Optional[str] = None | ||
| 22 | + | ||
| 23 | + def __post_init__(self) -> None: | ||
| 24 | + basic_auth = (self.client_id, self.client_secret) | ||
| 25 | + | ||
| 26 | + if not any(basic_auth): | ||
| 27 | + self.basic_auth = None | ||
| 28 | + return | ||
| 29 | + | ||
| 30 | + if not all(basic_auth): | ||
| 31 | + raise TypeError("Both client_id and client_secret must be defined") | ||
| 32 | + | ||
| 33 | + self.basic_auth = basic_auth | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,7 @@ | |||
| 2 | 2 | import requests | |
| 3 | 3 | ||
| 4 | 4 | import gitlab | |
| 5 | + from gitlab.oauth import PasswordCredentials | ||
| 5 | 6 | ||
| 6 | 7 | ||
| 7 | 8 | @pytest.fixture( | |
@@ -22,6 +23,13 @@ def test_auth_from_config(gl, gitlab_config, temp_dir): | |||
| 22 | 23 | assert isinstance(test_gitlab.user, gitlab.v4.objects.CurrentUser) | |
| 23 | 24 | ||
| 24 | 25 | ||
| 26 | + def test_auth_with_ropc_flow(gl, temp_dir): | ||
| 27 | + oauth_credentials = PasswordCredentials("root", "5iveL!fe") | ||
| 28 | + test_gitlab = gitlab.Gitlab(gl.url, oauth_credentials=oauth_credentials) | ||
| 29 | + test_gitlab.auth() | ||
| 30 | + assert isinstance(test_gitlab.user, gitlab.v4.objects.CurrentUser) | ||
| 31 | + | ||
| 32 | + | ||
| 25 | 33 | def test_no_custom_session(gl, temp_dir): | |
| 26 | 34 | """Test no custom session""" | |
| 27 | 35 | custom_session = requests.Session() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,8 +1,35 @@ | |||
| 1 | 1 | import pytest | |
| 2 | - import requests | ||
| 2 | + import responses | ||
| 3 | 3 | ||
| 4 | 4 | from gitlab import Gitlab | |
| 5 | 5 | from gitlab.config import GitlabConfigParser | |
| 6 | + from gitlab.oauth import PasswordCredentials | ||
| 7 | + | ||
| 8 | + | ||
| 9 | + # /oauth/token endpoint might be missing correct content-type header | ||
| 10 | + @pytest.fixture(params=["application/json", None]) | ||
| 11 | + def resp_oauth_token(gl: Gitlab, request: pytest.FixtureRequest): | ||
| 12 | + ropc_payload = { | ||
| 13 | + "username": "foo", | ||
| 14 | + "password": "bar", | ||
| 15 | + "grant_type": "password", | ||
| 16 | + "scope": "api", | ||
| 17 | + } | ||
| 18 | + ropc_response = { | ||
| 19 | + "access_token": "test-token", | ||
| 20 | + "token_type": "bearer", | ||
| 21 | + "expires_in": 7200, | ||
| 22 | + } | ||
| 23 | + with responses.RequestsMock() as rsps: | ||
| 24 | + rsps.add( | ||
| 25 | + method=responses.POST, | ||
| 26 | + url=f"{gl._base_url}/oauth/token", | ||
| 27 | + status=201, | ||
| 28 | + match=[responses.matchers.json_params_matcher(ropc_payload)], | ||
| 29 | + json=ropc_response, | ||
| 30 | + content_type=request.param, | ||
| 31 | + ) | ||
| 32 | + yield rsps | ||
| 6 | 33 | ||
| 7 | 34 | ||
| 8 | 35 | def test_invalid_auth_args(): | |
@@ -42,7 +69,6 @@ def test_private_token_auth(): | |||
| 42 | 69 | assert gl.private_token == "private_token" | |
| 43 | 70 | assert gl.oauth_token is None | |
| 44 | 71 | assert gl.job_token is None | |
| 45 | - assert gl._http_auth is None | ||
| 46 | 72 | assert "Authorization" not in gl.headers | |
| 47 | 73 | assert gl.headers["PRIVATE-TOKEN"] == "private_token" | |
| 48 | 74 | assert "JOB-TOKEN" not in gl.headers | |
@@ -53,7 +79,6 @@ def test_oauth_token_auth(): | |||
| 53 | 79 | assert gl.private_token is None | |
| 54 | 80 | assert gl.oauth_token == "oauth_token" | |
| 55 | 81 | assert gl.job_token is None | |
| 56 | - assert gl._http_auth is None | ||
| 57 | 82 | assert gl.headers["Authorization"] == "Bearer oauth_token" | |
| 58 | 83 | assert "PRIVATE-TOKEN" not in gl.headers | |
| 59 | 84 | assert "JOB-TOKEN" not in gl.headers | |
@@ -64,26 +89,38 @@ def test_job_token_auth(): | |||
| 64 | 89 | assert gl.private_token is None | |
| 65 | 90 | assert gl.oauth_token is None | |
| 66 | 91 | assert gl.job_token == "CI_JOB_TOKEN" | |
| 67 | - assert gl._http_auth is None | ||
| 68 | 92 | assert "Authorization" not in gl.headers | |
| 69 | 93 | assert "PRIVATE-TOKEN" not in gl.headers | |
| 70 | 94 | assert gl.headers["JOB-TOKEN"] == "CI_JOB_TOKEN" | |
| 71 | 95 | ||
| 72 | 96 | ||
| 73 | - def test_http_auth(): | ||
| 97 | + def test_oauth_resource_password_auth(resp_oauth_token): | ||
| 98 | + oauth_credentials = PasswordCredentials("foo", "bar") | ||
| 74 | 99 | gl = Gitlab( | |
| 75 | 100 | "http://localhost", | |
| 76 | - private_token="private_token", | ||
| 77 | - http_username="foo", | ||
| 78 | - http_password="bar", | ||
| 79 | 101 | api_version="4", | |
| 102 | + oauth_credentials=oauth_credentials, | ||
| 80 | 103 | ) | |
| 81 | - assert gl.private_token == "private_token" | ||
| 82 | - assert gl.oauth_token is None | ||
| 104 | + assert gl.oauth_token == "test-token" | ||
| 105 | + assert gl.private_token is None | ||
| 83 | 106 | assert gl.job_token is None | |
| 84 | - assert isinstance(gl._http_auth, requests.auth.HTTPBasicAuth) | ||
| 85 | - assert gl.headers["PRIVATE-TOKEN"] == "private_token" | ||
| 86 | - assert "Authorization" not in gl.headers | ||
| 107 | + assert "Authorization" in gl.headers | ||
| 108 | + assert "PRIVATE-TOKEN" not in gl.headers | ||
| 109 | + | ||
| 110 | + | ||
| 111 | + def test_oauth_resource_password_auth_with_legacy_params_warns(resp_oauth_token): | ||
| 112 | + with pytest.warns(DeprecationWarning, match="use the OAuth ROPC flow"): | ||
| 113 | + gl = Gitlab( | ||
| 114 | + "http://localhost", | ||
| 115 | + http_username="foo", | ||
| 116 | + http_password="bar", | ||
| 117 | + api_version="4", | ||
| 118 | + ) | ||
| 119 | + assert gl.oauth_token == "test-token" | ||
| 120 | + assert gl.private_token is None | ||
| 121 | + assert gl.job_token is None | ||
| 122 | + assert "Authorization" in gl.headers | ||
| 123 | + assert "PRIVATE-TOKEN" not in gl.headers | ||
| 87 | 124 | ||
| 88 | 125 | ||
| 89 | 126 | @pytest.mark.parametrize( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,27 @@ | |||
| 1 | + import pytest | ||
| 2 | + | ||
| 3 | + from gitlab.oauth import PasswordCredentials | ||
| 4 | + | ||
| 5 | + | ||
| 6 | + def test_password_credentials_without_password_raises(): | ||
| 7 | + with pytest.raises(TypeError, match="missing 1 required positional argument"): | ||
| 8 | + PasswordCredentials("username") | ||
| 9 | + | ||
| 10 | + | ||
| 11 | + def test_password_credentials_with_client_id_without_client_secret_raises(): | ||
| 12 | + with pytest.raises(TypeError, match="client_id and client_secret must be defined"): | ||
| 13 | + PasswordCredentials( | ||
| 14 | + "username", | ||
| 15 | + "password", | ||
| 16 | + client_id="abcdef123456", | ||
| 17 | + ) | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + def test_password_credentials_with_client_credentials_sets_basic_auth(): | ||
| 21 | + credentials = PasswordCredentials( | ||
| 22 | + "username", | ||
| 23 | + "password", | ||
| 24 | + client_id="abcdef123456", | ||
| 25 | + client_secret="123456abcdef", | ||
| 26 | + ) | ||
| 27 | + assert credentials.basic_auth == ("abcdef123456", "123456abcdef") | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments