Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f421b38
[ADD] webservice
etobella Dec 10, 2020
5165b7c
[UPD] Update webservice.pot
oca-travis Mar 15, 2021
7a6f9e9
[UPD] README.rst
OCA-git-bot Mar 15, 2021
175d1ba
[MIG] webservice: Migration to 14.0
etobella May 6, 2021
dc9b328
[UPD] Update webservice.pot
oca-travis May 14, 2021
600e567
[UPD] README.rst
OCA-git-bot May 14, 2021
a2cbe62
Added translation using Weblate (French)
Yvesldff Jun 17, 2021
e7bddfd
Translated using Weblate (French)
Yvesldff Jun 17, 2021
aa9a22f
[FIX] webservice: server.env.mixin needs to be inherited
LoisRForgeFlow Jun 21, 2021
cb13aa4
[UPD] Update webservice.pot
oca-travis Jul 30, 2021
d746d40
webservice 14.0.1.0.1
OCA-git-bot Jul 30, 2021
f7c3573
Update translation files
oca-transbot Jul 30, 2021
549e932
[MIG] webservice: Migration to 15.0
JasminSForgeFlow Feb 15, 2022
fa820a0
[UPD] Update webservice.pot
Feb 16, 2022
94792a0
[UPD] README.rst
OCA-git-bot Feb 16, 2022
329f868
[UPD] Update webservice.pot
Apr 7, 2022
0f12872
Update translation files
oca-transbot Apr 8, 2022
f49e883
webservice: move to web-api
simahawk Aug 10, 2022
4b5524e
[UPD] README.rst
OCA-git-bot Aug 10, 2022
3993358
[MIG] webservice: Migration to 16.0
EvaSForgeFlow Jul 10, 2023
b3f192b
webservice: improve call
simahawk Aug 27, 2022
0392a61
webservice: add api key and public auth support
simahawk Sep 1, 2022
f47b771
[UPD] Update webservice.pot
Jul 25, 2023
332c96f
[UPD] README.rst
OCA-git-bot Jul 25, 2023
213a197
Update translation files
weblate Jul 25, 2023
dffaeea
[UPD] README.rst
OCA-git-bot Sep 3, 2023
915d176
Added translation using Weblate (Italian)
mymage Nov 27, 2023
41070d5
Translated using Weblate (Italian)
mymage Nov 27, 2023
584369e
Translated using Weblate (Italian)
mymage Nov 28, 2023
62e9cca
Translated using Weblate (Italian)
mymage Jan 5, 2024
88ce7c8
[IMP] webservice: multi-company
JordiMForgeFlow Feb 2, 2024
4493168
[UPD] Update webservice.pot
Feb 5, 2024
da09c48
[BOT] post-merge updates
OCA-git-bot Feb 5, 2024
83d55d8
Update translation files
weblate Feb 5, 2024
59facea
Translated using Weblate (Italian)
mymage Feb 8, 2024
a499feb
[IMP] webservice: combine the url with collection's url
gurneyalex Feb 28, 2024
8c6865e
[BOT] post-merge updates
OCA-git-bot Apr 8, 2024
69e72d4
[IMP] webservice: add support for oauth2
gurneyalex Feb 28, 2024
da00695
add support for oauth2 web application flow
gurneyalex Apr 16, 2024
a0093a9
fixup! add support for oauth2 web application flow
gurneyalex Apr 24, 2024
3a80548
Translated using Weblate (Italian)
mymage May 13, 2024
5c0d6fd
[UPD] Update webservice.pot
May 14, 2024
87ddafe
[BOT] post-merge updates
OCA-git-bot May 14, 2024
454bbbf
Update translation files
weblate May 14, 2024
75957f0
Translated using Weblate (Italian)
mymage May 23, 2024
a6c8ee0
[IMP] webservice: pre-commit auto fixes
john-herholz-dt Aug 18, 2024
dbac0d4
[MIG] webservice: Migration to 17.0
john-herholz-dt Aug 18, 2024
a1e3db7
[DON'T MERGE] test-requirements.txt
john-herholz-dt Aug 18, 2024
fa4bfa3
fix test request handler mock
john-herholz-dt Aug 18, 2024
1058370
[FIX] webservice: WARNING message in logs
gurneyalex May 31, 2024
6c500fd
fix test request handler mock
john-herholz-dt Aug 18, 2024
5754ef4
fix test request handler mock
john-herholz-dt Aug 18, 2024
8fd1588
fix test request handler mock
john-herholz-dt Aug 18, 2024
d30861d
fix test request handler mock
john-herholz-dt Aug 18, 2024
324674d
fix test request handler mock
john-herholz-dt Aug 18, 2024
87a8bf9
fix test request handler mock
john-herholz-dt Aug 18, 2024
15c3d47
fix test request handler mock
john-herholz-dt Aug 18, 2024
f3124ff
fix test request handler mock
john-herholz-dt Aug 18, 2024
2a07516
fix test request handler mock
john-herholz-dt Aug 18, 2024
d8d19e2
fix test request handler mock
john-herholz-dt Aug 18, 2024
ca55530
fix test request handler mock
john-herholz-dt Aug 18, 2024
8bc7327
fix test request handler mock
john-herholz-dt Aug 18, 2024
ff6c146
fix test request handler mock
john-herholz-dt Aug 18, 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:121c86ad41210631fb9ae34436323a42c1d704ae098de31f93ce8f7c259140ce
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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 <etobella@creublanca.es>
- Alexandre Fayolle <alexandre.fayolle@camptocamp.com>

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
25 changes: 25 additions & 0 deletions webservice/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2020 Creu Blanca
# Copyright 2022 Camptocamp SA
# @author Simone Orsi <simahawk@gmail.com>
# 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
19 changes: 19 additions & 0 deletions webservice/components/base_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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 <simahawk@gmail.com>
# 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

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
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