Skip to content

Commit

Permalink
feat: handle 2-legged OAuth2 authentication method
Browse files Browse the repository at this point in the history
Signed-off-by: Adrien Barreau <[email protected]>
  • Loading branch information
deathiop committed Jun 27, 2024
1 parent 3f40c7d commit e4bebb2
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 55 deletions.
52 changes: 34 additions & 18 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.. image:: https://github.com/ovh/python-ovh/raw/master/docs/img/logo.png
:alt: Python & OVH APIs
:alt: Python & OVHcloud APIs
:target: https://pypi.python.org/pypi/ovh

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

- `OVH Europe <https://eu.api.ovh.com/createApp/>`_
- `OVH US <https://api.us.ovhcloud.com/createApp/>`_
- `OVH North-America <https://ca.api.ovh.com/createApp/>`_
- `OVHcloud Europe <https://eu.api.ovh.com/createApp/>`_
- `OVHcloud US <https://api.us.ovhcloud.com/createApp/>`_
- `OVHcloud North-America <https://ca.api.ovh.com/createApp/>`_
- `So you Start Europe <https://eu.api.soyoustart.com/createApp/>`_
- `So you Start North America <https://ca.api.soyoustart.com/createApp/>`_
- `Kimsufi Europe <https://eu.api.kimsufi.com/createApp/>`_
Expand Down Expand Up @@ -104,12 +104,15 @@ it looks like:
; uncomment following line when writing a script application
; with a single consumer key.
;consumer_key=my_consumer_key
; uncomment to enable oauth2 authentication
;client_id=my_client_id
;client_secret=my_client_secret
Depending on the API you want to use, you may set the ``endpoint`` to:

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

3. Authorize your application to access a customer account
**********************************************************
3. Authorize your application to access a customer account using OAuth2
***********************************************************************

``python-ovh`` supports two forms of authentication:
* OAuth2, using scopped service accounts, and compatible with OVHcloud IAM
* application key & application secret & consumer key (covered in the next chapter)

For OAuth2, first, you need to generate a pair of valid ``client_id`` and ``client_secret``: you
can proceed by [following this documentation](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343)

Once you have retrieved your ``client_id`` and ``client_secret``, you can create and edit
a configuration file that will be used by ``python-ovh``.

4. Authorize your application to access a customer account using custom OVHcloud authentication
***********************************************************************************************

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

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

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

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

Expand Down Expand Up @@ -359,7 +375,7 @@ You have 3 ways to provide configuration to the client:
Embed the configuration in the code
-----------------------------------

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

Expand Down Expand Up @@ -547,25 +563,25 @@ build HTML documentation:
Supported APIs
==============

OVH Europe
----------
OVHcloud Europe
---------------

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

OVH US
----------
OVHcloud US
-----------

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

OVH North America
-----------------
OVHcloud North America
---------------------

- **Documentation**: https://ca.api.ovh.com/
- **Community support**: [email protected]
Expand Down
88 changes: 76 additions & 12 deletions ovh/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
BadParametersError,
Forbidden,
HTTPError,
InvalidConfiguration,
InvalidCredential,
InvalidKey,
InvalidRegion,
Expand All @@ -60,8 +61,9 @@
ResourceExpiredError,
ResourceNotFoundError,
)
from .oauth2 import OAuth2

#: Mapping between OVH API region names and corresponding endpoints
# Mapping between OVH API region names and corresponding endpoints
ENDPOINTS = {
"ovh-eu": "https://eu.api.ovh.com/1.0",
"ovh-us": "https://api.us.ovhcloud.com/1.0",
Expand All @@ -72,9 +74,16 @@
"soyoustart-ca": "https://ca.api.soyoustart.com/1.0",
}

#: Default timeout for each request. 180 seconds connect, 180 seconds read.
# Default timeout for each request. 180 seconds connect, 180 seconds read.
TIMEOUT = 180

# OAuth2 token provider URLs
OAUTH2_TOKEN_URLS = {
"ovh-eu": "https://www.ovh.com/auth/oauth2/token",
"ovh-ca": "https://ca.ovh.com/auth/oauth2/token",
"ovh-us": "https://us.ovhcloud.com/auth/oauth2/token",
}


class Client:
"""
Expand Down Expand Up @@ -116,18 +125,24 @@ def __init__(
consumer_key=None,
timeout=TIMEOUT,
config_file=None,
client_id=None,
client_secret=None,
):
"""
Creates a new Client. No credential check is done at this point.
The ``application_key`` identifies your application while
``application_secret`` authenticates it. On the other hand, the
``consumer_key`` uniquely identifies your application's end user without
requiring his personal password.
When using OAuth2 authentication, ``client_id`` and ``client_secret``
will be used to initiate a Client Credential OAuth2 flow.
When using the OVHcloud authentication method, the ``application_key``
identifies your application while ``application_secret`` authenticates
it. On the other hand, the ``consumer_key`` uniquely identifies your
application's end user without requiring his personal password.
If any of ``endpoint``, ``application_key``, ``application_secret``
or ``consumer_key`` is not provided, this client will attempt to locate
from them from environment, ~/.ovh.cfg or /etc/ovh.cfg.
If any of ``endpoint``, ``application_key``, ``application_secret``,
``consumer_key``, ``client_id`` or ``client_secret`` is not provided,
this client will attempt to locate from them from environment,
``~/.ovh.cfg`` or ``/etc/ovh.cfg``.
See :py:mod:`ovh.config` for more information on supported
configuration mechanisms.
Expand All @@ -139,9 +154,11 @@ def __init__(
180 seconds for connection and 180 seconds for read.
:param str endpoint: API endpoint to use. Valid values in ``ENDPOINTS``
:param str application_key: Application key as provided by OVH
:param str application_secret: Application secret key as provided by OVH
:param str application_key: Application key as provided by OVHcloud
:param str application_secret: Application secret key as provided by OVHcloud
:param str consumer_key: uniquely identifies
:param str client_id: OAuth2 client ID
:param str client_secret: OAuth2 client secret
:param tuple timeout: Connection and read timeout for each request
:param float timeout: Same timeout for both connection and read
:raises InvalidRegion: if ``endpoint`` can't be found in ``ENDPOINTS``.
Expand Down Expand Up @@ -175,6 +192,50 @@ def __init__(
consumer_key = configuration.get(endpoint, "consumer_key")
self._consumer_key = consumer_key

# load OAuth2 data
if client_id is None:
client_id = configuration.get(endpoint, "client_id")
self._client_id = client_id

if client_secret is None:
client_secret = configuration.get(endpoint, "client_secret")
self._client_secret = client_secret

# configuration validation
if bool(self._client_id) is not bool(self._client_secret):
raise InvalidConfiguration("Invalid OAuth2 config, both client_id and client_secret must be given")

if bool(self._application_key) is not bool(self._application_secret):
raise InvalidConfiguration(
"Invalid authentication config, both application_key and application_secret must be given"
)

if self._client_id is not None and self._application_key is not None:
raise InvalidConfiguration(
"Can't use both application_key/application_secret and OAuth2 client_id/client_secret"
)
if self._client_id is None and self._application_key is None:
raise InvalidConfiguration(
"Missing authentication information, you need to provide at least an application_key/application_secret"
" or a client_id/client_secret"
)
if self._client_id and endpoint not in OAUTH2_TOKEN_URLS:
raise InvalidConfiguration(
"OAuth2 authentication is not compatible with endpoint "
+ endpoint
+ " (it can only be used with ovh-eu, ovh-ca and ovh-us)"
)

# when in OAuth2 mode, instantiate the oauthlib client
if self._client_id:
self._oauth2 = OAuth2(
client_id=self._client_id,
client_secret=self._client_secret,
token_url=OAUTH2_TOKEN_URLS[endpoint],
)
else:
self._oauth2 = None

# lazy load time delta
self._time_delta = None

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

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

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

# sign request. Never sign 'time' or will recurse infinitely
if need_auth:
if self._oauth2:
return self._oauth2.session.request(method, target, headers=headers, data=body, timeout=self._timeout)

if not self._application_secret:
raise InvalidKey("Invalid ApplicationSecret '%s'" % self._application_secret)

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

headers["X-Ovh-Application"] = self._application_key
return self._session.request(method, target, headers=headers, data=body, timeout=self._timeout)
2 changes: 2 additions & 0 deletions ovh/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
application_key=my_app_key
application_secret=my_application_secret
consumer_key=my_consumer_key
client_id=my_client_id
client_secret=my_client_secret
The client will successively attempt to locate this configuration file in
Expand Down
8 changes: 8 additions & 0 deletions ovh/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class InvalidCredential(APIError):
"""Raised when trying to sign request with invalid consumer key"""


class InvalidConfiguration(APIError):
"""Raised when trying to load an invalid configuration into a client"""


class InvalidResponse(APIError):
"""Raised when api response is not valid json"""

Expand Down Expand Up @@ -101,3 +105,7 @@ class Forbidden(APIError):

class ResourceExpiredError(APIError):
"""Raised when requested resource expired."""


class OAuth2FailureError(APIError):
"""Raised when the OAuth2 workflow fails"""
Loading

0 comments on commit e4bebb2

Please sign in to comment.