Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit90be806

Browse files
committed
feat(client): replace basic auth with OAuth ROPC flow
1 parent3f86d36 commit90be806

File tree

7 files changed

+182
-30
lines changed

7 files changed

+182
-30
lines changed

‎docs/api-usage.rst

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,32 @@ Note on password authentication
8484

8585
GitLab has long removed password-based basic authentication. You can currently still use the
8686
`resource owner password credentials<https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow>`_
87-
flowtoobtain an OAuth token.
87+
flowand python-gitlab willobtain an OAuth token for you when instantiated.
8888

8989
However, we do not recommend this as it will not work with 2FA enabled, and GitLab is removing
90-
ROPC-based flows without clientIDs 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 clientcredentials in a future release. We recommend you obtain tokens for
91+
automated workflows.
9292

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.oauthimport 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)
95113
96114
Managers
97115
========

‎docs/cli-usage.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,7 @@ We recommend that you use `Credential helpers`_ to securely store your tokens.
168168
<https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html>`__
169169
to learn how to obtain a token.
170170
* - ``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.
173172
* - ``job_token``
174173
- Your job token. See `the official documentation
175174
<https://docs.gitlab.com/ce/api/jobs.html#get-job-artifacts>`__

‎gitlab/client.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
importgitlab.config
1414
importgitlab.const
1515
importgitlab.exceptions
16-
fromgitlabimporthttp_backends,utils
16+
fromgitlabimporthttp_backends,oauth,utils
1717

1818
REDIRECT_MSG= (
1919
"python-gitlab detected a {status_code} ({reason!r}) redirection. You must update "
@@ -42,8 +42,8 @@ class Gitlab:
4242
the value is a string, it is the path to a CA file used for
4343
certificate validation.
4444
timeout: Timeout to use for requests to the GitLab server.
45-
http_username: Username forHTTP authentication
46-
http_password: Password forHTTP authentication
45+
http_username: Username forOAuth ROPC flow (deprecated, use oauth_credentials)
46+
http_password: Password forOAuth ROPC flow (deprecated, use oauth_credentials)
4747
api_version: Gitlab API version to use (support for 4 only)
4848
pagination: Can be set to 'keyset' to use keyset pagination
4949
order_by: Set order_by globally
@@ -52,6 +52,7 @@ class Gitlab:
5252
or 52x responses. Defaults to False.
5353
keep_base_url: keep user-provided base URL for pagination if it
5454
differs from response headers
55+
oauth_credentials: Password credentials for authenticating via OAuth ROPC flow
5556
5657
Keyward Args:
5758
requests.Session session: Http Requests Session
@@ -75,6 +76,8 @@ def __init__(
7576
user_agent:str=gitlab.const.USER_AGENT,
7677
retry_transient_errors:bool=False,
7778
keep_base_url:bool=False,
79+
*,
80+
oauth_credentials:Optional[oauth.PasswordCredentials]=None,
7881
**kwargs:Any,
7982
)->None:
8083
self._api_version=str(api_version)
@@ -97,13 +100,15 @@ def __init__(
97100
self.http_password=http_password
98101
self.oauth_token=oauth_token
99102
self.job_token=job_token
100-
self._set_auth_info()
103+
self.oauth_credentials=oauth_credentials
101104

102105
#: Create a session object for requests
103106
http_backend:Type[http_backends.DefaultBackend]=kwargs.pop(
104107
"http_backend",http_backends.DefaultBackend
105108
)
106109
self.http_backend=http_backend(**kwargs)
110+
111+
self._set_auth_info()
107112
self.session=self.http_backend.client
108113

109114
self.per_page=per_page
@@ -513,28 +518,53 @@ def _set_auth_info(self) -> None:
513518
"Only one of oauth authentication or http "
514519
"authentication should be defined"
515520
)
516-
517521
self._http_auth=None
518522
ifself.private_token:
519523
self.headers.pop("Authorization",None)
520524
self.headers["PRIVATE-TOKEN"]=self.private_token
521525
self.headers.pop("JOB-TOKEN",None)
526+
return
527+
528+
ifnotself.oauth_credentialsand (self.http_usernameandself.http_password):
529+
utils.warn(
530+
"Passing http_username and http_password is deprecated and will be "
531+
"removed in a future version.\nPlease use the OAuth ROPC flow with"
532+
"(gitlab.oauth.PasswordCredentials) if you need password-based"
533+
"authentication. See https://docs.gitlab.com/ee/api/oauth2.html"
534+
"#resource-owner-password-credentials-flow for more details.",
535+
category=DeprecationWarning,
536+
)
537+
self.oauth_credentials=oauth.PasswordCredentials(
538+
self.http_username,self.http_password
539+
)
540+
541+
ifself.oauth_credentials:
542+
post_data= {
543+
"grant_type":self.oauth_credentials.grant_type,
544+
"scope":self.oauth_credentials.scope,
545+
"username":self.oauth_credentials.username,
546+
"password":self.oauth_credentials.password,
547+
}
548+
response=self.http_post(
549+
f"{self._base_url}/oauth/token",post_data=post_data
550+
)
551+
ifisinstance(response,dict):
552+
self.oauth_token=response["access_token"]
553+
else:
554+
self.oauth_token=response.json()["access_token"]
555+
self._http_auth=self.oauth_credentials.basic_auth
522556

523557
ifself.oauth_token:
524558
self.headers["Authorization"]=f"Bearer{self.oauth_token}"
525559
self.headers.pop("PRIVATE-TOKEN",None)
526560
self.headers.pop("JOB-TOKEN",None)
561+
return
527562

528563
ifself.job_token:
529564
self.headers.pop("Authorization",None)
530565
self.headers.pop("PRIVATE-TOKEN",None)
531566
self.headers["JOB-TOKEN"]=self.job_token
532567

533-
ifself.http_username:
534-
self._http_auth=requests.auth.HTTPBasicAuth(
535-
self.http_username,self.http_password
536-
)
537-
538568
@staticmethod
539569
defenable_debug()->None:
540570
importlogging

‎gitlab/oauth.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
importdataclasses
2+
fromtypingimportOptional
3+
4+
5+
@dataclasses.dataclass
6+
classPasswordCredentials:
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+
ifnotany(basic_auth):
27+
self.basic_auth=None
28+
return
29+
30+
ifnotall(basic_auth):
31+
raiseTypeError("Both client_id and client_secret must be defined")
32+
33+
self.basic_auth=basic_auth

‎tests/functional/api/test_gitlab.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
importrequests
33

44
importgitlab
5+
fromgitlab.oauthimportPasswordCredentials
56

67

78
@pytest.fixture(
@@ -22,6 +23,13 @@ def test_auth_from_config(gl, gitlab_config, temp_dir):
2223
assertisinstance(test_gitlab.user,gitlab.v4.objects.CurrentUser)
2324

2425

26+
deftest_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+
assertisinstance(test_gitlab.user,gitlab.v4.objects.CurrentUser)
31+
32+
2533
deftest_no_custom_session(gl,temp_dir):
2634
"""Test no custom session"""
2735
custom_session=requests.Session()

‎tests/unit/test_gitlab_auth.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
11
importpytest
2-
importrequests
2+
importresponses
33

44
fromgitlabimportGitlab
55
fromgitlab.configimportGitlabConfigParser
6+
fromgitlab.oauthimportPasswordCredentials
7+
8+
9+
# /oauth/token endpoint might be missing correct content-type header
10+
@pytest.fixture(params=["application/json",None])
11+
defresp_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+
withresponses.RequestsMock()asrsps:
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+
yieldrsps
633

734

835
deftest_invalid_auth_args():
@@ -42,7 +69,6 @@ def test_private_token_auth():
4269
assertgl.private_token=="private_token"
4370
assertgl.oauth_tokenisNone
4471
assertgl.job_tokenisNone
45-
assertgl._http_authisNone
4672
assert"Authorization"notingl.headers
4773
assertgl.headers["PRIVATE-TOKEN"]=="private_token"
4874
assert"JOB-TOKEN"notingl.headers
@@ -53,7 +79,6 @@ def test_oauth_token_auth():
5379
assertgl.private_tokenisNone
5480
assertgl.oauth_token=="oauth_token"
5581
assertgl.job_tokenisNone
56-
assertgl._http_authisNone
5782
assertgl.headers["Authorization"]=="Bearer oauth_token"
5883
assert"PRIVATE-TOKEN"notingl.headers
5984
assert"JOB-TOKEN"notingl.headers
@@ -64,26 +89,38 @@ def test_job_token_auth():
6489
assertgl.private_tokenisNone
6590
assertgl.oauth_tokenisNone
6691
assertgl.job_token=="CI_JOB_TOKEN"
67-
assertgl._http_authisNone
6892
assert"Authorization"notingl.headers
6993
assert"PRIVATE-TOKEN"notingl.headers
7094
assertgl.headers["JOB-TOKEN"]=="CI_JOB_TOKEN"
7195

7296

73-
deftest_http_auth():
97+
deftest_oauth_resource_password_auth(resp_oauth_token):
98+
oauth_credentials=PasswordCredentials("foo","bar")
7499
gl=Gitlab(
75100
"http://localhost",
76-
private_token="private_token",
77-
http_username="foo",
78-
http_password="bar",
79101
api_version="4",
102+
oauth_credentials=oauth_credentials,
80103
)
81-
assertgl.private_token=="private_token"
82-
assertgl.oauth_tokenisNone
104+
assertgl.oauth_token=="test-token"
105+
assertgl.private_tokenisNone
83106
assertgl.job_tokenisNone
84-
assertisinstance(gl._http_auth,requests.auth.HTTPBasicAuth)
85-
assertgl.headers["PRIVATE-TOKEN"]=="private_token"
86-
assert"Authorization"notingl.headers
107+
assert"Authorization"ingl.headers
108+
assert"PRIVATE-TOKEN"notingl.headers
109+
110+
111+
deftest_oauth_resource_password_auth_with_legacy_params_warns(resp_oauth_token):
112+
withpytest.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+
assertgl.oauth_token=="test-token"
120+
assertgl.private_tokenisNone
121+
assertgl.job_tokenisNone
122+
assert"Authorization"ingl.headers
123+
assert"PRIVATE-TOKEN"notingl.headers
87124

88125

89126
@pytest.mark.parametrize(

‎tests/unit/test_oauth.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
importpytest
2+
3+
fromgitlab.oauthimportPasswordCredentials
4+
5+
6+
deftest_password_credentials_without_password_raises():
7+
withpytest.raises(TypeError,match="missing 1 required positional argument"):
8+
PasswordCredentials("username")
9+
10+
11+
deftest_password_credentials_with_client_id_without_client_secret_raises():
12+
withpytest.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+
deftest_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+
assertcredentials.basic_auth== ("abcdef123456","123456abcdef")

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp