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

Commit3733872

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

File tree

7 files changed

+159
-48
lines changed

7 files changed

+159
-48
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: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
importgitlab.config
1313
importgitlab.const
1414
importgitlab.exceptions
15-
fromgitlabimport_backends,utils
15+
fromgitlabimport_backends,oauth,utils
1616

1717
REDIRECT_MSG= (
1818
"python-gitlab detected a {status_code} ({reason!r}) redirection. You must update "
@@ -41,8 +41,6 @@ class Gitlab:
4141
the value is a string, it is the path to a CA file used for
4242
certificate validation.
4343
timeout: Timeout to use for requests to the GitLab server.
44-
http_username: Username for HTTP authentication
45-
http_password: Password for HTTP authentication
4644
api_version: Gitlab API version to use (support for 4 only)
4745
pagination: Can be set to 'keyset' to use keyset pagination
4846
order_by: Set order_by globally
@@ -51,6 +49,7 @@ class Gitlab:
5149
or 52x responses. Defaults to False.
5250
keep_base_url: keep user-provided base URL for pagination if it
5351
differs from response headers
52+
oauth_credentials: Password credentials for authenticating via OAuth ROPC flow
5453
5554
Keyword Args:
5655
requests.Session session: HTTP Requests Session
@@ -64,8 +63,6 @@ def __init__(
6463
oauth_token:Optional[str]=None,
6564
job_token:Optional[str]=None,
6665
ssl_verify:Union[bool,str]=True,
67-
http_username:Optional[str]=None,
68-
http_password:Optional[str]=None,
6966
timeout:Optional[float]=None,
7067
api_version:str="4",
7168
per_page:Optional[int]=None,
@@ -74,6 +71,8 @@ def __init__(
7471
user_agent:str=gitlab.const.USER_AGENT,
7572
retry_transient_errors:bool=False,
7673
keep_base_url:bool=False,
74+
*,
75+
oauth_credentials:Optional[oauth.PasswordCredentials]=None,
7776
**kwargs:Any,
7877
)->None:
7978
self._api_version=str(api_version)
@@ -92,11 +91,9 @@ def __init__(
9291
self.ssl_verify=ssl_verify
9392

9493
self.private_token=private_token
95-
self.http_username=http_username
96-
self.http_password=http_password
9794
self.oauth_token=oauth_token
9895
self.job_token=job_token
99-
self._set_auth_info()
96+
self.oauth_credentials=oauth_credentials
10097

10198
#: Create a session object for requests
10299
_backend:Type[_backends.DefaultBackend]=kwargs.pop(
@@ -105,6 +102,7 @@ def __init__(
105102
self._backend=_backend(**kwargs)
106103
self.session=self._backend.client
107104

105+
self._set_auth_info()
108106
self.per_page=per_page
109107
self.pagination=pagination
110108
self.order_by=order_by
@@ -271,8 +269,6 @@ def from_config(
271269
job_token=config.job_token,
272270
ssl_verify=config.ssl_verify,
273271
timeout=config.timeout,
274-
http_username=config.http_username,
275-
http_password=config.http_password,
276272
api_version=config.api_version,
277273
per_page=config.per_page,
278274
pagination=config.pagination,
@@ -471,41 +467,51 @@ def set_license(self, license: str, **kwargs: Any) -> Dict[str, Any]:
471467
returnresult
472468

473469
def_set_auth_info(self)->None:
474-
tokens= [
475-
token
476-
fortokenin [self.private_token,self.oauth_token,self.job_token]
477-
iftoken
470+
auth_types= [
471+
auth
472+
forauthin [
473+
self.private_token,
474+
self.oauth_token,
475+
self.oauth_credentials,
476+
self.job_token,
477+
]
478+
ifauth
478479
]
479-
iflen(tokens)>1:
480+
iflen(auth_types)>1:
480481
raiseValueError(
481-
"Only one of private_token, oauth_token or job_token should "
482-
"be defined"
483-
)
484-
if (self.http_usernameandnotself.http_password)or (
485-
notself.http_usernameandself.http_password
486-
):
487-
raiseValueError("Both http_username and http_password should be defined")
488-
iftokensandself.http_username:
489-
raiseValueError(
490-
"Only one of token authentications or http "
491-
"authentication should be defined"
482+
"Only one of private_token, oauth_token, oauth_credentials"
483+
"or job_token should be defined"
492484
)
493485

494486
self._auth:Optional[requests.auth.AuthBase]=None
495487
ifself.private_token:
496488
self._auth=_backends.PrivateTokenAuth(self.private_token)
489+
return
497490

498491
ifself.oauth_token:
499492
self._auth=_backends.OAuthTokenAuth(self.oauth_token)
493+
return
494+
495+
ifself.oauth_credentials:
496+
post_data= {
497+
"grant_type":self.oauth_credentials.grant_type,
498+
"scope":self.oauth_credentials.scope,
499+
"username":self.oauth_credentials.username,
500+
"password":self.oauth_credentials.password,
501+
}
502+
response=self.http_post(
503+
f"{self._base_url}/oauth/token",post_data=post_data
504+
)
505+
ifisinstance(response,dict):
506+
self.oauth_token=response["access_token"]
507+
else:
508+
self.oauth_token=response.json()["access_token"]
509+
self._auth=self.oauth_credentials.basic_auth
510+
return
500511

501512
ifself.job_token:
502513
self._auth=_backends.JobTokenAuth(self.job_token)
503514

504-
ifself.http_usernameandself.http_password:
505-
self._auth=requests.auth.HTTPBasicAuth(
506-
self.http_username,self.http_password
507-
)
508-
509515
@staticmethod
510516
defenable_debug()->None:
511517
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: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@
88
fromgitlabimportGitlab
99
fromgitlab._backendsimportJobTokenAuth,OAuthTokenAuth,PrivateTokenAuth
1010
fromgitlab.configimportGitlabConfigParser
11+
fromgitlab.oauthimportPasswordCredentials
12+
13+
14+
# /oauth/token endpoint might be missing correct content-type header
15+
@pytest.fixture(params=["application/json",None])
16+
defresp_oauth_token(gl:Gitlab,request:pytest.FixtureRequest):
17+
ropc_payload= {
18+
"username":"foo",
19+
"password":"bar",
20+
"grant_type":"password",
21+
"scope":"api",
22+
}
23+
ropc_response= {
24+
"access_token":"test-token",
25+
"token_type":"bearer",
26+
"expires_in":7200,
27+
}
28+
withresponses.RequestsMock()asrsps:
29+
rsps.add(
30+
method=responses.POST,
31+
url=f"{gl._base_url}/oauth/token",
32+
status=201,
33+
match=[responses.matchers.json_params_matcher(ropc_payload)],
34+
json=ropc_response,
35+
content_type=request.param,
36+
)
37+
yieldrsps
1138

1239

1340
@pytest.fixture
@@ -91,24 +118,17 @@ def test_job_token_auth():
91118
assert"Authorization"notinp.headers
92119

93120

94-
deftest_http_auth():
95-
gl=Gitlab(
96-
"http://localhost",
97-
http_username="foo",
98-
http_password="bar",
99-
api_version="4",
100-
)
121+
deftest_oauth_resource_password_auth(resp_oauth_token):
122+
oauth_credentials=PasswordCredentials("foo","bar")
123+
gl=Gitlab("http://localhost",oauth_credentials=oauth_credentials)
101124
p=PreparedRequest()
102125
p.prepare(url=gl.url,auth=gl._auth)
103126
assertgl.private_tokenisNone
104127
assertgl.oauth_tokenisNone
105128
assertgl.job_tokenisNone
106-
assertisinstance(gl._auth,requests.auth.HTTPBasicAuth)
107129
assertgl._auth.username=="foo"
108130
assertgl._auth.password=="bar"
109131
assertp.headers["Authorization"]=="Basic Zm9vOmJhcg=="
110-
assert"PRIVATE-TOKEN"notinp.headers
111-
assert"JOB-TOKEN"notinp.headers
112132

113133

114134
@responses.activate

‎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