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

[17.0][MIG] webservice: Migration to 17.0 #55

Merged
merged 50 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7931136
[ADD] webservice
etobella Dec 10, 2020
d042cfc
[UPD] Update webservice.pot
oca-travis Mar 15, 2021
dc33d42
[UPD] README.rst
OCA-git-bot Mar 15, 2021
2f5a487
[MIG] webservice: Migration to 14.0
etobella May 6, 2021
8053369
[UPD] Update webservice.pot
oca-travis May 14, 2021
40269bb
[UPD] README.rst
OCA-git-bot May 14, 2021
1bc9b11
Added translation using Weblate (French)
Yvesldff Jun 17, 2021
48f53c8
Translated using Weblate (French)
Yvesldff Jun 17, 2021
ed93f96
[FIX] webservice: server.env.mixin needs to be inherited
LoisRForgeFlow Jun 21, 2021
e82fc60
[UPD] Update webservice.pot
oca-travis Jul 30, 2021
52b821b
webservice 14.0.1.0.1
OCA-git-bot Jul 30, 2021
f7325e7
Update translation files
oca-transbot Jul 30, 2021
ae8818e
[MIG] webservice: Migration to 15.0
JasminSForgeFlow Feb 15, 2022
0476ce0
[UPD] Update webservice.pot
Feb 16, 2022
817df91
[UPD] README.rst
OCA-git-bot Feb 16, 2022
afbace4
[UPD] Update webservice.pot
Apr 7, 2022
1a62eed
Update translation files
oca-transbot Apr 8, 2022
549332e
webservice: move to web-api
simahawk Aug 10, 2022
cc2b54d
[UPD] README.rst
OCA-git-bot Aug 10, 2022
a4ad314
[MIG] webservice: Migration to 16.0
EvaSForgeFlow Jul 10, 2023
f8b5de1
webservice: improve call
simahawk Aug 27, 2022
82eb15e
webservice: add api key and public auth support
simahawk Sep 1, 2022
c6826cc
[UPD] Update webservice.pot
Jul 25, 2023
b0cb662
[UPD] README.rst
OCA-git-bot Jul 25, 2023
c11b33b
Update translation files
weblate Jul 25, 2023
81fde2f
[UPD] README.rst
OCA-git-bot Sep 3, 2023
8eba096
Added translation using Weblate (Italian)
mymage Nov 27, 2023
ea8496b
Translated using Weblate (Italian)
mymage Nov 27, 2023
1f492ad
Translated using Weblate (Italian)
mymage Nov 28, 2023
ebbc314
Translated using Weblate (Italian)
mymage Jan 5, 2024
c4fdfd7
[IMP] webservice: multi-company
JordiMForgeFlow Feb 2, 2024
1197d88
[UPD] Update webservice.pot
Feb 5, 2024
94bdcc4
[BOT] post-merge updates
OCA-git-bot Feb 5, 2024
4ad9cb8
Update translation files
weblate Feb 5, 2024
b2623bd
Translated using Weblate (Italian)
mymage Feb 8, 2024
a478764
[IMP] webservice: combine the url with collection's url
gurneyalex Feb 28, 2024
d551c97
[BOT] post-merge updates
OCA-git-bot Apr 8, 2024
8255649
[IMP] webservice: add support for oauth2
gurneyalex Feb 28, 2024
4d95fc0
add support for oauth2 web application flow
gurneyalex Apr 16, 2024
4d63e8c
fixup! add support for oauth2 web application flow
gurneyalex Apr 24, 2024
8f58a24
Translated using Weblate (Italian)
mymage May 13, 2024
1a4aa69
[UPD] Update webservice.pot
May 14, 2024
bf96745
[BOT] post-merge updates
OCA-git-bot May 14, 2024
a7cb36d
Update translation files
weblate May 14, 2024
3050fb9
Translated using Weblate (Italian)
mymage May 23, 2024
0836989
[FIX] webservice: WARNING message in logs
gurneyalex May 31, 2024
73a4fa5
[BOT] post-merge updates
OCA-git-bot Sep 16, 2024
c750c9d
[IMP] webservice: pre-commit auto fixes
SilvioC2C Sep 17, 2024
6f28230
[MIG] webservice: Migration to 17.0
SilvioC2C Sep 17, 2024
0398355
[IMP] webservice: improve tests
SilvioC2C Sep 17, 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
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# generated from manifests external_dependencies
oauthlib
requests-oauthlib
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
responses
86 changes: 86 additions & 0 deletions webservice/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
==========
WebService
==========

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:44ac7b2e56db131da94f2a4ed85bb452eae8657d761490ffa97b08b268d6f9fe
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
:target: https://odoo-community.org/page/development-status
: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%2Fweb--api-lightgray.png?logo=github
:target: https://github.com/OCA/web-api/tree/17.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/web-api-17-0/web-api-17-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/web-api&target_branch=17.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module creates WebService frameworks to be used globally

**Table of contents**

.. contents::
:local:

Bug Tracker
===========

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/web-api/issues/new?body=module:%20webservice%0Aversion:%2017.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.

Credits
=======

Authors
-------

* Creu Blanca
* Camptocamp

Contributors
------------

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

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

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.

.. |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/17.0/webservice>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
3 changes: 3 additions & 0 deletions webservice/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import components
from . import models
from . import controllers
23 changes: 23 additions & 0 deletions webservice/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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": "17.0.1.0.0",
"license": "AGPL-3",
"development_status": "Production/Stable",
"maintainers": ["etobella"],
"author": "Creu Blanca, Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/web-api",
"depends": ["component", "server_environment"],
"external_dependencies": {"python": ["requests-oauthlib", "oauthlib"]},
"data": [
"security/ir.model.access.csv",
"security/ir_rule.xml",
"views/webservice_backend.xml",
],
"demo": [],
}
2 changes: 2 additions & 0 deletions webservice/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import base_adapter
from . import request_adapter
20 changes: 20 additions & 0 deletions webservice/components/base_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo.addons.component.core import AbstractComponent


class BaseWebServiceAdapter(AbstractComponent):
_name = "base.webservice.adapter"
_collection = "webservice.backend"
_webservice_protocol = False
_usage = "webservice.request"

@classmethod
def _component_match(cls, work, usage=None, model_name=None, **kw):
"""Override to customize match.

Registry lookup filtered by usage and model_name when landing here.
Now, narrow match to `_match_attrs` attributes.
"""
return kw.get("webservice_protocol") in (None, cls._webservice_protocol)
247 changes: 247 additions & 0 deletions webservice/components/request_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# 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"

# 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),
"timeout": None,
}
)
# pylint: disable=E8106
request = requests.request(method, url, **new_kwargs)
request.raise_for_status()
return request.content

def get(self, **kwargs):
return self._request("get", **kwargs)

def post(self, **kwargs):
return self._request("post", **kwargs)

def put(self, **kwargs):
return self._request("put", **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

Check warning on line 58 in webservice/components/request_adapter.py

View check run for this annotation

Codecov / codecov/patch

webservice/components/request_adapter.py#L58

Added line #L58 was not covered by tests

def _get_headers(self, content_type=False, headers=False, **kwargs):
headers = headers or {}
result = {
"Content-Type": content_type or self.collection.content_type,
}
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

Check warning on line 122 in webservice/components/request_adapter.py

View check run for this annotation

Codecov / codecov/patch

webservice/components/request_adapter.py#L122

Added line #L122 was not covered by tests
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

Check warning on line 134 in webservice/components/request_adapter.py

View check run for this annotation

Codecov / codecov/patch

webservice/components/request_adapter.py#L134

Added line #L134 was not covered by tests
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(

Check warning on line 189 in webservice/components/request_adapter.py

View check run for this annotation

Codecov / codecov/patch

webservice/components/request_adapter.py#L189

Added line #L189 was not covered by tests
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