Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[13.0][BKP][IMP]webservice #1061

Closed
wants to merge 51 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2273b3a
[RM]webservice
GuillemCForgeFlow Oct 31, 2024
558af1e
[ADD] webservice
etobella Dec 10, 2020
fafa068
[UPD] Update webservice.pot
oca-travis Mar 15, 2021
c871820
[UPD] README.rst
OCA-git-bot Mar 15, 2021
ff0ec23
[MIG] webservice: Migration to 14.0
etobella May 6, 2021
7bd3a62
[UPD] Update webservice.pot
oca-travis May 14, 2021
6cc8492
[UPD] README.rst
OCA-git-bot May 14, 2021
a766c93
Added translation using Weblate (French)
Yvesldff Jun 17, 2021
82e7035
Translated using Weblate (French)
Yvesldff Jun 17, 2021
5854715
[FIX] webservice: server.env.mixin needs to be inherited
LoisRForgeFlow Jun 21, 2021
153a7bf
[UPD] Update webservice.pot
oca-travis Jul 30, 2021
505cabb
webservice 14.0.1.0.1
OCA-git-bot Jul 30, 2021
c9e82e5
Update translation files
oca-transbot Jul 30, 2021
6e3956c
[MIG] webservice: Migration to 15.0
JasminSForgeFlow Feb 15, 2022
0d2bcf5
[UPD] Update webservice.pot
Feb 16, 2022
767e3b9
[UPD] README.rst
OCA-git-bot Feb 16, 2022
6850e25
[UPD] Update webservice.pot
Apr 7, 2022
e45a238
Update translation files
oca-transbot Apr 8, 2022
5675459
webservice: move to web-api
simahawk Aug 10, 2022
324a8f9
[UPD] README.rst
OCA-git-bot Aug 10, 2022
e1f5e4c
[MIG] webservice: Migration to 16.0
EvaSForgeFlow Jul 10, 2023
353940c
webservice: improve call
simahawk Aug 27, 2022
9064c48
webservice: add api key and public auth support
simahawk Sep 1, 2022
76f8376
[UPD] Update webservice.pot
Jul 25, 2023
10625f2
[UPD] README.rst
OCA-git-bot Jul 25, 2023
70c1ee3
Update translation files
weblate Jul 25, 2023
fa1eddc
[UPD] README.rst
OCA-git-bot Sep 3, 2023
b863bec
Added translation using Weblate (Italian)
mymage Nov 27, 2023
7093da2
Translated using Weblate (Italian)
mymage Nov 27, 2023
23721ea
Translated using Weblate (Italian)
mymage Nov 28, 2023
e73382c
Translated using Weblate (Italian)
mymage Jan 5, 2024
263ed23
[IMP] webservice: multi-company
JordiMForgeFlow Feb 2, 2024
f8b215b
[UPD] Update webservice.pot
Feb 5, 2024
7c3f8e6
[BOT] post-merge updates
OCA-git-bot Feb 5, 2024
a0228ec
Update translation files
weblate Feb 5, 2024
8864776
Translated using Weblate (Italian)
mymage Feb 8, 2024
5544a2d
[IMP] webservice: combine the url with collection's url
gurneyalex Feb 28, 2024
e0e78f1
[BOT] post-merge updates
OCA-git-bot Apr 8, 2024
4a512a5
[IMP] webservice: add support for oauth2
gurneyalex Feb 28, 2024
6239235
add support for oauth2 web application flow
gurneyalex Apr 16, 2024
a4630b9
fixup! add support for oauth2 web application flow
gurneyalex Apr 24, 2024
fe97377
Translated using Weblate (Italian)
mymage May 13, 2024
d81d3a4
[UPD] Update webservice.pot
May 14, 2024
c2c42d8
[BOT] post-merge updates
OCA-git-bot May 14, 2024
8ce2de5
Update translation files
weblate May 14, 2024
38b77cf
Translated using Weblate (Italian)
mymage May 23, 2024
131198d
[FIX] webservice: WARNING message in logs
gurneyalex May 31, 2024
35f9b4e
[BOT] post-merge updates
OCA-git-bot Sep 16, 2024
c7f7bbc
Translated using Weblate (Italian)
mymage Oct 29, 2024
398d179
[IMP]webservice: black, isort, prettier
GuillemCForgeFlow Oct 31, 2024
1df6fa6
[BKP][IMP]webservice
GuillemCForgeFlow Oct 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# generated from manifests external_dependencies
factur-x
lxml
oauthlib
pyyaml
requests-oauthlib
xmlschema
32 changes: 21 additions & 11 deletions webservice/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@ WebService
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5f94eb3f289e9c45b302e0793f0b45a5b32bde37eefe1d8908f0fae84997495d
!! source digest: sha256:44ac7b2e56db131da94f2a4ed85bb452eae8657d761490ffa97b08b268d6f9fe
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
:alt: Production/Stable
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github
:target: https://github.com/OCA/edi/tree/13.0/webservice
:alt: OCA/edi
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb--api-lightgray.png?logo=github
:target: https://github.com/OCA/web-api/tree/16.0/webservice
:alt: OCA/web-api
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/edi-13-0/edi-13-0-webservice
:target: https://translation.odoo-community.org/projects/web-api-16-0/web-api-16-0-webservice
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=13.0
:target: https://runboat.odoo-community.org/builds?repo=OCA/web-api&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|
Expand All @@ -38,10 +38,10 @@ This module creates WebService frameworks to be used globally
Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/edi/issues>`_.
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web-api/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/edi/issues/new?body=module:%20webservice%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/web-api/issues/new?body=module:%20webservice%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Expand All @@ -52,11 +52,13 @@ Authors
~~~~~~~

* Creu Blanca
* Camptocamp

Contributors
~~~~~~~~~~~~

* Enric Tobella <[email protected]>
* Alexandre Fayolle <[email protected]>

Maintainers
~~~~~~~~~~~
Expand All @@ -71,6 +73,14 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/edi <https://github.com/OCA/edi/tree/13.0/webservice>`_ project on GitHub.
.. |maintainer-etobella| image:: https://github.com/etobella.png?size=40px
:target: https://github.com/etobella
:alt: etobella

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-etobella|

This module is part of the `OCA/web-api <https://github.com/OCA/web-api/tree/16.0/webservice>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions webservice/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from . import components
from . import models
from . import controllers
17 changes: 13 additions & 4 deletions webservice/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
# Copyright 2020 Creu Blanca
# Copyright 2022 Camptocamp SA
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).


{
"name": "WebService",
"summary": """
Defines webservice abstract definition to be used generally""",
"version": "13.0.1.0.2",
"version": "13.0.1.0.0",
"license": "AGPL-3",
"development_status": "Beta",
"author": "Creu Blanca,Odoo Community Association (OCA)",
"development_status": "Production/Stable",
"maintainers": ["etobella"],
"author": "Creu Blanca, Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/edi",
"depends": ["component", "server_environment"],
"data": ["security/ir.model.access.csv", "views/webservice_backend.xml"],
"external_dependencies": {"python": ["requests-oauthlib", "oauthlib"]},
"data": [
"security/ir.model.access.csv",
"security/ir_rule.xml",
"views/webservice_backend.xml",
],
"demo": [],
}
207 changes: 200 additions & 7 deletions webservice/components/request_adapter.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
# Copyright 2020 Creu Blanca
# Copyright 2022 Camptocamp SA
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import json
import logging
import time

import requests
from oauthlib.oauth2 import BackendApplicationClient, WebApplicationClient
from requests_oauthlib import OAuth2Session

from odoo.addons.component.core import Component

_logger = logging.getLogger(__name__)


class BaseRestRequestsAdapter(Component):
_name = "base.requests"
_webservice_protocol = "http"
_inherit = "base.webservice.adapter"

def _request(self, method, **kwargs):
# TODO: url and url_params could come from work_ctx
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(
{"auth": self._get_auth(**kwargs), "headers": self._get_headers(**kwargs)}
)
request = requests.request(
method, self.collection.url.format(**kwargs), **new_kwargs
{
"auth": self._get_auth(**kwargs),
"headers": self._get_headers(**kwargs),
"timeout": None,
}
)
# pylint: disable=E8106
request = requests.request(method, url, **new_kwargs)
request.raise_for_status()
return request.content

Expand All @@ -34,14 +49,192 @@ def put(self, **kwargs):
def _get_auth(self, auth=False, **kwargs):
if auth:
return auth
handler = getattr(self, "_get_auth_for_" + self.collection.auth_type, None)
return handler(**kwargs) if handler else None

def _get_auth_for_user_pwd(self, **kw):
if self.collection.username and self.collection.password:
return self.collection.username, self.collection.password
return None

def _get_headers(self, content_type=False, headers=False, **kwargs):
headers = headers or {}
result = {
"Content-Type": content_type or self.collection.content_type,
}
if isinstance(headers, dict):
result.update(headers)
handler = getattr(self, "_get_headers_for_" + self.collection.auth_type, None)
if handler:
headers.update(handler(**kwargs))
result.update(headers)
return result

def _get_headers_for_api_key(self, **kw):
return {self.collection.api_key_header: self.collection.api_key}

def _get_url(self, url=None, url_params=None, **kwargs):
if not url:
url = self.collection.url
elif not url.startswith(self.collection.url):
if not url.startswith("http"):
url = f"{self.collection.url.rstrip('/')}/{url.lstrip('/')}"
else:
# TODO: if url is given, we should validate the domain
# to avoid abusing a webservice backend for different calls.
pass

url_params = url_params or kwargs
return url.format(**url_params)


class BackendApplicationOAuth2RestRequestsAdapter(Component):
_name = "oauth2.requests.backend.application"
_webservice_protocol = "http+oauth2-backend_application"
_inherit = "base.requests"

def get_client(self, oauth_params: dict):
return BackendApplicationClient(client_id=oauth_params["oauth2_clientid"])

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",
"redirect_url",
]
)[0]
client = self.get_client(oauth_params)
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


class WebApplicationOAuth2RestRequestsAdapter(Component):
_name = "oauth2.requests.web.application"
_webservice_protocol = "http+oauth2-web_application"
_inherit = "oauth2.requests.backend.application"

def get_client(self, oauth_params: dict):
return WebApplicationClient(
client_id=oauth_params["oauth2_clientid"],
code=oauth_params.get("oauth2_autorization"),
redirect_uri=oauth_params["redirect_url"],
)

def _fetch_token_from_authorization(self, authorization_code):

oauth_params = self.collection.sudo().read(
[
"oauth2_clientid",
"oauth2_client_secret",
"oauth2_token_url",
"oauth2_audience",
"redirect_url",
]
)[0]
client = WebApplicationClient(client_id=oauth_params["oauth2_clientid"])

with OAuth2Session(
client=client, redirect_uri=oauth_params.get("redirect_url")
) as session:
token = session.fetch_token(
oauth_params["oauth2_token_url"],
client_secret=oauth_params["oauth2_client_secret"],
code=authorization_code,
audience=oauth_params.get("oauth2_audience") or "",
include_client_id=True,
)
return token

def redirect_to_authorize(self, **authorization_url_extra_params):
"""set the oauth2_state on the backend
:return: the webservice authorization url with the proper parameters
"""
# we are normally authenticated at this stage, so no need to sudo()
backend = self.collection
oauth_params = backend.read(
[
"oauth2_clientid",
"oauth2_token_url",
"oauth2_audience",
"oauth2_authorization_url",
"oauth2_scope",
"redirect_url",
]
)[0]
client = WebApplicationClient(client_id=oauth_params["oauth2_clientid"],)

with OAuth2Session(
client=client, redirect_uri=oauth_params.get("redirect_url"),
) as session:
authorization_url, state = session.authorization_url(
backend.oauth2_authorization_url, **authorization_url_extra_params
)
backend.oauth2_state = state
return authorization_url
1 change: 1 addition & 0 deletions webservice/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import oauth2
Loading
Loading