-
-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[IMP] webservice: add support for oauth2
Allow using oauth2 with Backend Application Flow / Client Credentials Grant.
- Loading branch information
1 parent
ca571de
commit bad61b1
Showing
11 changed files
with
305 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# generated from manifests external_dependencies | ||
requests-oauthlib |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ WebService | |
!! This file is generated by oca-gen-addon-readme !! | ||
!! changes will be overwritten. !! | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
!! source digest: sha256:3c8e3b258f995f0eb2c12d74904d41e163f8e67c98cb545dd100c1575e105904 | ||
!! source digest: sha256:a554d8b0213fbcf9d0a3516b0eb65705733d42498e3c009f6105dea07ae6da67 | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png | ||
|
@@ -58,6 +58,7 @@ Contributors | |
~~~~~~~~~~~~ | ||
|
||
* Enric Tobella <[email protected]> | ||
* Alexandre Fayolle <[email protected]> | ||
|
||
Maintainers | ||
~~~~~~~~~~~ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,12 @@ | |
# @author Simone Orsi <[email protected]> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
import json | ||
import time | ||
|
||
import requests | ||
from oauthlib.oauth2 import BackendApplicationClient | ||
from requests_oauthlib import OAuth2Session | ||
|
||
from odoo.addons.component.core import Component | ||
|
||
|
@@ -65,3 +70,93 @@ def _get_url(self, url=None, url_params=None, **kwargs): | |
url = self.collection.url | ||
url_params = url_params or kwargs | ||
return url.format(**url_params) | ||
|
||
|
||
class OAuth2RestRequestsAdapter(Component): | ||
_name = "oauth2.requests" | ||
_webservice_protocol = "http+oauth2" | ||
_inherit = "base.requests" | ||
|
||
def __init__(self, *args, **kw): | ||
super().__init__(*args, **kw) | ||
# cached value to avoid hitting the database each time we need the token | ||
self._token = {} | ||
|
||
def _is_token_valid(self, token): | ||
"""Validate given oauth2 token. | ||
We consider that a token in valid if it has at least 10% of | ||
its valid duration. So if a token has a validity of 1h, we will | ||
renew it if we try to use it 6 minutes before its expiration date. | ||
""" | ||
expires_at = token.get("expires_at", 0) | ||
expires_in = token.get("expires_in", 3600) # default to 1h | ||
now = time.time() | ||
return now <= (expires_at - 0.1 * expires_in) | ||
|
||
@property | ||
def token(self): | ||
"""Return a valid oauth2 token. | ||
The tokens are stored in the database, and we check if they are still | ||
valid, and renew them if needed. | ||
""" | ||
if self._is_token_valid(self._token): | ||
return self._token | ||
backend = self.collection | ||
with backend.env.registry.cursor() as cr: | ||
cr.execute( | ||
"SELECT oauth2_token FROM webservice_backend " | ||
"WHERE id=%s " | ||
"FOR NO KEY UPDATE", # prevent concurrent token fetching | ||
(backend.id,), | ||
) | ||
token_str = cr.fetchone()[0] or "{}" | ||
token = json.loads(token_str) | ||
if self._is_token_valid(token): | ||
self._token = token | ||
else: | ||
new_token = self._fetch_new_token(old_token=token) | ||
cr.execute( | ||
"UPDATE webservice_backend " "SET oauth2_token=%s " "WHERE id=%s", | ||
(json.dumps(new_token), backend.id), | ||
) | ||
self._token = new_token | ||
return self._token | ||
|
||
def _fetch_new_token(self, old_token): | ||
# TODO: check if the old token has a refresh_token that can | ||
# be used (and use it in that case) | ||
oauth_params = self.collection.sudo().read( | ||
[ | ||
"oauth2_clientid", | ||
"oauth2_client_secret", | ||
"oauth2_token_url", | ||
"oauth2_audience", | ||
] | ||
)[0] | ||
client = BackendApplicationClient(client_id=oauth_params["oauth2_clientid"]) | ||
with OAuth2Session(client=client) as session: | ||
token = session.fetch_token( | ||
token_url=oauth_params["oauth2_token_url"], | ||
cliend_id=oauth_params["oauth2_clientid"], | ||
client_secret=oauth_params["oauth2_client_secret"], | ||
audience=oauth_params.get("oauth2_audience") or "", | ||
) | ||
return token | ||
|
||
def _request(self, method, url=None, url_params=None, **kwargs): | ||
url = self._get_url(url=url, url_params=url_params) | ||
new_kwargs = kwargs.copy() | ||
new_kwargs.update( | ||
{ | ||
"headers": self._get_headers(**kwargs), | ||
"timeout": None, | ||
} | ||
) | ||
client = BackendApplicationClient(client_id=self.collection.oauth2_clientid) | ||
with OAuth2Session(client=client, token=self.token) as session: | ||
# pylint: disable=E8106 | ||
request = session.request(method, url, **new_kwargs) | ||
request.raise_for_status() | ||
return request.content |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,10 @@ | ||
# Copyright 2020 Creu Blanca | ||
# Copyright 2022 Camptocamp SA | ||
# @author Simone Orsi <[email protected]> | ||
# @author Alexandre Fayolle <[email protected]> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
|
||
from odoo import _, api, exceptions, fields, models | ||
|
||
|
||
|
@@ -21,14 +23,22 @@ class WebserviceBackend(models.Model): | |
("none", "Public"), | ||
("user_pwd", "Username & password"), | ||
("api_key", "API Key"), | ||
("oauth2", "OAuth2 Backend Application Flow (Client Credentials Grant)"), | ||
], | ||
default="user_pwd", | ||
required=True, | ||
) | ||
username = fields.Char(auth_type="user_pwd") | ||
password = fields.Char(auth_type="user_pwd") | ||
api_key = fields.Char(string="API Key", auth_type="api_key") | ||
api_key_header = fields.Char(string="API Key header", auth_type="api_key") | ||
oauth2_clientid = fields.Char(string="Client ID", auth_type="oauth2") | ||
oauth2_client_secret = fields.Char(string="Client Secret", auth_type="oauth2") | ||
oauth2_token_url = fields.Char(string="Token URL", auth_type="oauth2") | ||
oauth2_audience = fields.Char( | ||
string="Audience" | ||
# no auth_type because not required | ||
) | ||
oauth2_token = fields.Char(help="the OAuth2 token (serialized JSON)") | ||
content_type = fields.Selection( | ||
[ | ||
("application/json", "JSON"), | ||
|
@@ -77,9 +87,16 @@ def call(self, method, *args, **kwargs): | |
def _get_adapter(self): | ||
with self.work_on(self._name) as work: | ||
return work.component( | ||
usage="webservice.request", webservice_protocol=self.protocol | ||
usage="webservice.request", | ||
webservice_protocol=self._get_adapter_protocol(), | ||
) | ||
|
||
def _get_adapter_protocol(self): | ||
protocol = self.protocol | ||
if self.auth_type.startswith("oauth2"): | ||
protocol += f"+{self.auth_type}" | ||
return protocol | ||
|
||
@property | ||
def _server_env_fields(self): | ||
base_fields = super()._server_env_fields | ||
|
@@ -92,6 +109,10 @@ def _server_env_fields(self): | |
"api_key": {}, | ||
"api_key_header": {}, | ||
"content_type": {}, | ||
"oauth2_clientid": {}, | ||
"oauth2_client_secret": {}, | ||
"oauth2_token_url": {}, | ||
"oauth2_audience": {}, | ||
} | ||
webservice_fields.update(base_fields) | ||
return webservice_fields |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
* Enric Tobella <[email protected]> | ||
* Alexandre Fayolle <[email protected]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
from . import test_webservice | ||
from . import test_webservice, test_oauth2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.