Skip to content

Commit a5d8ae6

Browse files
authored
Merge pull request #134 from deathiop/origin/oauth2
feat: handle Client Credential OAuth2 authentication method
2 parents ac8e9db + d1b0c78 commit a5d8ae6

12 files changed

+423
-55
lines changed

README.rst

+34-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.. image:: https://github.com/ovh/python-ovh/raw/master/docs/img/logo.png
2-
:alt: Python & OVH APIs
2+
:alt: Python & OVHcloud APIs
33
:target: https://pypi.python.org/pypi/ovh
44

55
Lightweight wrapper around OVHcloud's APIs. Handles all the hard work including
@@ -73,9 +73,9 @@ To interact with the APIs, the SDK needs to identify itself using an
7373
``application_key`` and an ``application_secret``. To get them, you need
7474
to register your application. Depending the API you plan to use, visit:
7575

76-
- `OVH Europe <https://eu.api.ovh.com/createApp/>`_
77-
- `OVH US <https://api.us.ovhcloud.com/createApp/>`_
78-
- `OVH North-America <https://ca.api.ovh.com/createApp/>`_
76+
- `OVHcloud Europe <https://eu.api.ovh.com/createApp/>`_
77+
- `OVHcloud US <https://api.us.ovhcloud.com/createApp/>`_
78+
- `OVHcloud North-America <https://ca.api.ovh.com/createApp/>`_
7979
- `So you Start Europe <https://eu.api.soyoustart.com/createApp/>`_
8080
- `So you Start North America <https://ca.api.soyoustart.com/createApp/>`_
8181
- `Kimsufi Europe <https://eu.api.kimsufi.com/createApp/>`_
@@ -104,12 +104,15 @@ it looks like:
104104
; uncomment following line when writing a script application
105105
; with a single consumer key.
106106
;consumer_key=my_consumer_key
107+
; uncomment to enable oauth2 authentication
108+
;client_id=my_client_id
109+
;client_secret=my_client_secret
107110
108111
Depending on the API you want to use, you may set the ``endpoint`` to:
109112

110-
* ``ovh-eu`` for OVH Europe API
111-
* ``ovh-us`` for OVH US API
112-
* ``ovh-ca`` for OVH North-America API
113+
* ``ovh-eu`` for OVHcloud Europe API
114+
* ``ovh-us`` for OVHcloud US API
115+
* ``ovh-ca`` for OVHcloud North-America API
113116
* ``soyoustart-eu`` for So you Start Europe API
114117
* ``soyoustart-ca`` for So you Start North America API
115118
* ``kimsufi-eu`` for Kimsufi Europe API
@@ -120,8 +123,21 @@ See Configuration_ for more information on available configuration mechanisms.
120123
.. note:: When using a versioning system, make sure to add ``ovh.conf`` to ignored
121124
files. It contains confidential/security-sensitive information!
122125

123-
3. Authorize your application to access a customer account
124-
**********************************************************
126+
3. Authorize your application to access a customer account using OAuth2
127+
***********************************************************************
128+
129+
``python-ovh`` supports two forms of authentication:
130+
* OAuth2, using scopped service accounts, and compatible with OVHcloud IAM
131+
* application key & application secret & consumer key (covered in the next chapter)
132+
133+
For OAuth2, first, you need to generate a pair of valid ``client_id`` and ``client_secret``: you
134+
can proceed by [following this documentation](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343)
135+
136+
Once you have retrieved your ``client_id`` and ``client_secret``, you can create and edit
137+
a configuration file that will be used by ``python-ovh``.
138+
139+
4. Authorize your application to access a customer account using custom OVHcloud authentication
140+
***********************************************************************************************
125141

126142
To allow your application to access a customer account using the API on your
127143
behalf, you need a **consumer key (CK)**.
@@ -164,7 +180,7 @@ Install a new mail redirection
164180
------------------------------
165181

166182
e-mail redirections may be freely configured on domains and DNS zones hosted by
167-
OVH to an arbitrary destination e-mail using API call
183+
OVHcloud to an arbitrary destination e-mail using API call
168184
``POST /email/domain/{domain}/redirection``.
169185

170186
For this call, the api specifies that the source address shall be given under the
@@ -195,7 +211,7 @@ is only supported with reserved keywords.
195211
Grab bill list
196212
--------------
197213

198-
Let's say you want to integrate OVH bills into your own billing system, you
214+
Let's say you want to integrate OVHcloud bills into your own billing system, you
199215
could just script around the ``/me/bills`` endpoints and even get the details
200216
of each bill lines using ``/me/bill/{billId}/details/{billDetailId}``.
201217

@@ -359,7 +375,7 @@ You have 3 ways to provide configuration to the client:
359375
Embed the configuration in the code
360376
-----------------------------------
361377

362-
The straightforward way to use OVH's API keys is to embed them directly in the
378+
The straightforward way to use OVHcloud's API keys is to embed them directly in the
363379
application code. While this is very convenient, it lacks of elegance and
364380
flexibility.
365381

@@ -547,25 +563,25 @@ build HTML documentation:
547563
Supported APIs
548564
==============
549565

550-
OVH Europe
551-
----------
566+
OVHcloud Europe
567+
---------------
552568

553569
- **Documentation**: https://eu.api.ovh.com/
554570
- **Community support**: [email protected]
555571
- **Console**: https://eu.api.ovh.com/console
556572
- **Create application credentials**: https://eu.api.ovh.com/createApp/
557573
- **Create script credentials** (all keys at once): https://eu.api.ovh.com/createToken/
558574

559-
OVH US
560-
----------
575+
OVHcloud US
576+
-----------
561577

562578
- **Documentation**: https://api.us.ovhcloud.com/
563579
- **Console**: https://api.us.ovhcloud.com/console/
564580
- **Create application credentials**: https://api.us.ovhcloud.com/createApp/
565581
- **Create script credentials** (all keys at once): https://api.us.ovhcloud.com/createToken/
566582

567-
OVH North America
568-
-----------------
583+
OVHcloud North America
584+
---------------------
569585

570586
- **Documentation**: https://ca.api.ovh.com/
571587
- **Community support**: [email protected]

ovh/client.py

+76-12
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
BadParametersError,
5050
Forbidden,
5151
HTTPError,
52+
InvalidConfiguration,
5253
InvalidCredential,
5354
InvalidKey,
5455
InvalidRegion,
@@ -60,8 +61,9 @@
6061
ResourceExpiredError,
6162
ResourceNotFoundError,
6263
)
64+
from .oauth2 import OAuth2
6365

64-
#: Mapping between OVH API region names and corresponding endpoints
66+
# Mapping between OVH API region names and corresponding endpoints
6567
ENDPOINTS = {
6668
"ovh-eu": "https://eu.api.ovh.com/1.0",
6769
"ovh-us": "https://api.us.ovhcloud.com/1.0",
@@ -72,9 +74,16 @@
7274
"soyoustart-ca": "https://ca.api.soyoustart.com/1.0",
7375
}
7476

75-
#: Default timeout for each request. 180 seconds connect, 180 seconds read.
77+
# Default timeout for each request. 180 seconds connect, 180 seconds read.
7678
TIMEOUT = 180
7779

80+
# OAuth2 token provider URLs
81+
OAUTH2_TOKEN_URLS = {
82+
"ovh-eu": "https://www.ovh.com/auth/oauth2/token",
83+
"ovh-ca": "https://ca.ovh.com/auth/oauth2/token",
84+
"ovh-us": "https://us.ovhcloud.com/auth/oauth2/token",
85+
}
86+
7887

7988
class Client:
8089
"""
@@ -116,18 +125,24 @@ def __init__(
116125
consumer_key=None,
117126
timeout=TIMEOUT,
118127
config_file=None,
128+
client_id=None,
129+
client_secret=None,
119130
):
120131
"""
121132
Creates a new Client. No credential check is done at this point.
122133
123-
The ``application_key`` identifies your application while
124-
``application_secret`` authenticates it. On the other hand, the
125-
``consumer_key`` uniquely identifies your application's end user without
126-
requiring his personal password.
134+
When using OAuth2 authentication, ``client_id`` and ``client_secret``
135+
will be used to initiate a Client Credential OAuth2 flow.
136+
137+
When using the OVHcloud authentication method, the ``application_key``
138+
identifies your application while ``application_secret`` authenticates
139+
it. On the other hand, the ``consumer_key`` uniquely identifies your
140+
application's end user without requiring his personal password.
127141
128-
If any of ``endpoint``, ``application_key``, ``application_secret``
129-
or ``consumer_key`` is not provided, this client will attempt to locate
130-
from them from environment, ~/.ovh.cfg or /etc/ovh.cfg.
142+
If any of ``endpoint``, ``application_key``, ``application_secret``,
143+
``consumer_key``, ``client_id`` or ``client_secret`` is not provided,
144+
this client will attempt to locate from them from environment,
145+
``~/.ovh.cfg`` or ``/etc/ovh.cfg``.
131146
132147
See :py:mod:`ovh.config` for more information on supported
133148
configuration mechanisms.
@@ -139,9 +154,11 @@ def __init__(
139154
180 seconds for connection and 180 seconds for read.
140155
141156
:param str endpoint: API endpoint to use. Valid values in ``ENDPOINTS``
142-
:param str application_key: Application key as provided by OVH
143-
:param str application_secret: Application secret key as provided by OVH
157+
:param str application_key: Application key as provided by OVHcloud
158+
:param str application_secret: Application secret key as provided by OVHcloud
144159
:param str consumer_key: uniquely identifies
160+
:param str client_id: OAuth2 client ID
161+
:param str client_secret: OAuth2 client secret
145162
:param tuple timeout: Connection and read timeout for each request
146163
:param float timeout: Same timeout for both connection and read
147164
:raises InvalidRegion: if ``endpoint`` can't be found in ``ENDPOINTS``.
@@ -175,6 +192,50 @@ def __init__(
175192
consumer_key = configuration.get(endpoint, "consumer_key")
176193
self._consumer_key = consumer_key
177194

195+
# load OAuth2 data
196+
if client_id is None:
197+
client_id = configuration.get(endpoint, "client_id")
198+
self._client_id = client_id
199+
200+
if client_secret is None:
201+
client_secret = configuration.get(endpoint, "client_secret")
202+
self._client_secret = client_secret
203+
204+
# configuration validation
205+
if bool(self._client_id) is not bool(self._client_secret):
206+
raise InvalidConfiguration("Invalid OAuth2 config, both client_id and client_secret must be given")
207+
208+
if bool(self._application_key) is not bool(self._application_secret):
209+
raise InvalidConfiguration(
210+
"Invalid authentication config, both application_key and application_secret must be given"
211+
)
212+
213+
if self._client_id is not None and self._application_key is not None:
214+
raise InvalidConfiguration(
215+
"Can't use both application_key/application_secret and OAuth2 client_id/client_secret"
216+
)
217+
if self._client_id is None and self._application_key is None:
218+
raise InvalidConfiguration(
219+
"Missing authentication information, you need to provide at least an application_key/application_secret"
220+
" or a client_id/client_secret"
221+
)
222+
if self._client_id and endpoint not in OAUTH2_TOKEN_URLS:
223+
raise InvalidConfiguration(
224+
"OAuth2 authentication is not compatible with endpoint "
225+
+ endpoint
226+
+ " (it can only be used with ovh-eu, ovh-ca and ovh-us)"
227+
)
228+
229+
# when in OAuth2 mode, instantiate the oauthlib client
230+
if self._client_id:
231+
self._oauth2 = OAuth2(
232+
client_id=self._client_id,
233+
client_secret=self._client_secret,
234+
token_url=OAUTH2_TOKEN_URLS[endpoint],
235+
)
236+
else:
237+
self._oauth2 = None
238+
178239
# lazy load time delta
179240
self._time_delta = None
180241

@@ -524,7 +585,6 @@ def raw_call(self, method, path, data=None, need_auth=True, headers=None):
524585

525586
if headers is None:
526587
headers = {}
527-
headers["X-Ovh-Application"] = self._application_key
528588

529589
# include payload
530590
if data is not None:
@@ -533,6 +593,9 @@ def raw_call(self, method, path, data=None, need_auth=True, headers=None):
533593

534594
# sign request. Never sign 'time' or will recurse infinitely
535595
if need_auth:
596+
if self._oauth2:
597+
return self._oauth2.session.request(method, target, headers=headers, data=body, timeout=self._timeout)
598+
536599
if not self._application_secret:
537600
raise InvalidKey("Invalid ApplicationSecret '%s'" % self._application_secret)
538601

@@ -551,4 +614,5 @@ def raw_call(self, method, path, data=None, need_auth=True, headers=None):
551614
headers["X-Ovh-Timestamp"] = now
552615
headers["X-Ovh-Signature"] = "$1$" + signature.hexdigest()
553616

617+
headers["X-Ovh-Application"] = self._application_key
554618
return self._session.request(method, target, headers=headers, data=body, timeout=self._timeout)

ovh/config.py

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
application_key=my_app_key
5050
application_secret=my_application_secret
5151
consumer_key=my_consumer_key
52+
client_id=my_client_id
53+
client_secret=my_client_secret
5254
5355
The client will successively attempt to locate this configuration file in
5456

ovh/exceptions.py

+8
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ class InvalidCredential(APIError):
5959
"""Raised when trying to sign request with invalid consumer key"""
6060

6161

62+
class InvalidConfiguration(APIError):
63+
"""Raised when trying to load an invalid configuration into a client"""
64+
65+
6266
class InvalidResponse(APIError):
6367
"""Raised when api response is not valid json"""
6468

@@ -101,3 +105,7 @@ class Forbidden(APIError):
101105

102106
class ResourceExpiredError(APIError):
103107
"""Raised when requested resource expired."""
108+
109+
110+
class OAuth2FailureError(APIError):
111+
"""Raised when the OAuth2 workflow fails"""

0 commit comments

Comments
 (0)