Browse Source

[ADD] webhook: Add standard webhook odoo module

pull/737/merge
Moisés López 7 years ago
committed by Jairo Llopis
parent
commit
c3436ec370
  1. 143
      webhook/README.rst
  2. 7
      webhook/__init__.py
  3. 30
      webhook/__openerp__.py
  4. 5
      webhook/controllers/__init__.py
  5. 44
      webhook/controllers/main.py
  6. 16
      webhook/data/webhook_data.xml
  7. 17
      webhook/demo/webhook_demo.xml
  8. 5
      webhook/models/__init__.py
  9. 205
      webhook/models/webhook.py
  10. 3
      webhook/security/ir.model.access.csv
  11. 5
      webhook/tests/__init__.py
  12. 124
      webhook/tests/test_webhook_post.py
  13. 44
      webhook/views/webhook_views.xml

143
webhook/README.rst

@ -0,0 +1,143 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
=======
Webhook
=======
Module to receive .. _global webhooks: https://en.wikipedia.org/wiki/Webhook events.
This module invoke methods to process webhook events.
Configuration
=============
You will need create a new module to add your logic to process the events with methods called:
*def run_CONSUMER_EVENT\**
Example with gihub consumer and push event.
.. code-block:: python
@api.one
def run_github_push_task(self):
# You will have all request data in
# variable: self.env.request
pass
Where CONSUMER is the name of you webhook consumer. e.g. github (Extract from field *name* of *webhook* model)
Where EVENT is the name of the event from webhook *request* data.
Where *\** is your particular method to process this event.
To configure a new webhook you need add all ip or subnet address (with *ip/integer*) owned by your webhook consumer in webhook.address model as data.
Example with github:
.. code-block:: xml
<!--webhook github data of remote address-->
<record model="webhook.address" id="webhook_address_github">
<field name="name">192.30.252.0/22</field>
<field name="webhook_id" ref="webhook_github"/>
</record>
You need to add a python code to extract event name from webhook request info into `python_code_get_event` field of webhook model.
You can get all full data of request webhook from variable `request`
Example with github:
.. code-block:: xml
<!--webhook github data-->
<record model="webhook" id="webhook_github">
<field name="name">github</field>
<field name="python_code_get_event">request.httprequest.headers.get('X-Github-Event')</field>
</record>
Full example of create a new webhook configuration data.
.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!--webhook github data-->
<record model="webhook" id="webhook_github">
<field name="name">github</field>
<field name="python_code_get_event">request.httprequest.headers.get('X-Github-Event')</field>
</record>
<!--webhook github data of remote address-->
<record model="webhook.address" id="webhook_address_github">
<field name="name">192.30.252.0/22</field>
<field name="webhook_id" ref="webhook_github"/>
</record>
</data>
</openerp>
.. figure:: path/to/local/image.png
:alt: alternative description
:width: 600 px
Usage
=====
To use this module, you need to:
#. Go to your customer webhook configuration from 3rd-party applications
and use the odoo webhook url HOST/webhook/NAME_WEBHOOK
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/{repo_id}/{branch}
.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt
.. branch is "8.0" for example
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Moisés López <moylop260@vauxoo.com>
Funders
-------
The development of this module has been financially supported by:
* Vauxoo
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
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.
To contribute to this module, please visit https://odoo-community.org.

7
webhook/__init__.py

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Vauxoo - https://www.vauxoo.com/
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models
from . import controllers
from . import tests

30
webhook/__openerp__.py

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Vauxoo - https://www.vauxoo.com/
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Webhook',
'version': '8.0.1.0.0',
'author': 'Vauxoo, Odoo Community Association (OCA)',
'category': 'Server Tools',
'website': 'https://www.vauxoo.com',
'license': 'AGPL-3',
'depends': [
'web',
],
'external_dependencies': {
'python': [
'ipaddress',
'requests',
],
},
'data': [
'security/ir.model.access.csv',
'views/webhook_views.xml',
'data/webhook_data.xml',
],
'demo': [
'demo/webhook_demo.xml',
],
'installable': True,
'auto_install': False,
}

5
webhook/controllers/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Vauxoo - https://www.vauxoo.com/
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import main

44
webhook/controllers/main.py

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Vauxoo - https://www.vauxoo.com/
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import pprint
from openerp.addons.web import http
from openerp.http import request
from openerp import SUPERUSER_ID, exceptions
from openerp.tools.translate import _
class WebhookController(http.Controller):
@http.route(['/webhook/<webhook_name>'], type='json',
auth='none', method=['POST'])
def webhook(self, webhook_name, **post):
'''
:params string webhook_name: Name of webhook to use
Webhook odoo controller to receive json request and send to
driver method.
You will need create your webhook with http://0.0.0.0:0000/webhook
NOTE: Important use --db-filter params in odoo start.
'''
# Deprecated by webhook_name dynamic name
# webhook = webhook_registry.search_with_request(
# cr, SUPERUSER_ID, request, context=context)
webhook = request.env['webhook'].with_env(
request.env(user=SUPERUSER_ID)).search(
[('name', '=', webhook_name)], limit=1)
# TODO: Add security by secret string or/and ip consumer
if not webhook:
remote_addr = ''
if hasattr(request, 'httprequest'):
if hasattr(request.httprequest, 'remote_addr'):
remote_addr = request.httprequest.remote_addr
raise exceptions.ValidationError(_(
'webhook consumer [%s] from remote address [%s] '
'not found jsonrequest [%s]' % (
webhook_name,
remote_addr,
pprint.pformat(request.jsonrequest)[:450]
)))
webhook.run_webhook(request)

16
webhook/data/webhook_data.xml

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!--webhook github data-->
<record model="webhook" id="webhook_github">
<field name="name">github</field>
<field name="python_code_get_event">request.httprequest.headers.get('X-Github-Event')</field>
</record>
<record model="webhook.address" id="webhook_address_github">
<field name="name">192.30.252.0/22</field>
<field name="webhook_id" ref="webhook_github"/>
</record>
</data>
</openerp>

17
webhook/demo/webhook_demo.xml

@ -0,0 +1,17 @@
<?xml version='1.0' encoding='UTF-8'?>
<openerp>
<data noupdate="1">
<record id="webhook_test" model="webhook">
<field name="name">wehook_test</field>
<field name="python_code_get_event">request.httprequest.headers.get("X-Webhook-Test-Event")</field>
<field name="python_code_get_ip">request.httprequest.headers.get("X-Webhook-Test-Address")</field>
</record>
<record id="webhook_address_localhost" model="webhook.address">
<field name="name">127.0.0.1</field>
<field name="webhook_id" ref="webhook_test"/>
</record>
</data>
</openerp>

5
webhook/models/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Vauxoo - https://www.vauxoo.com/
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import webhook

205
webhook/models/webhook.py

@ -0,0 +1,205 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Vauxoo - https://www.vauxoo.com/
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
import traceback
from openerp import api, exceptions, fields, models, tools
from openerp.tools.safe_eval import safe_eval
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
try:
import ipaddress
except ImportError as err:
_logger.debug(err)
class WebhookAddress(models.Model):
_name = 'webhook.address'
name = fields.Char(
'IP or Network Address',
required=True,
help='IP or network address of your consumer webhook:\n'
'ip address e.g.: 10.10.0.8\n'
'network address e.g. of: 10.10.0.8/24',
)
webhook_id = fields.Many2one(
'webhook', 'Webhook', required=True, ondelete='cascade')
class Webhook(models.Model):
_name = 'webhook'
name = fields.Char(
'Consumer name',
required=True,
help='Name of your consumer webhook. '
'This name will be used in named of event methods')
address_ids = fields.One2many(
'webhook.address', 'webhook_id', 'IP or Network Address',
required=True,
help='This address will be filter to know who is '
'consumer webhook')
python_code_get_event = fields.Text(
'Get event',
required=True,
help='Python code to get event from request data.\n'
'You have object.env.request variable with full '
'webhook request.',
default='# You can use object.env.request variable '
'to get full data of webhook request.\n'
'# Example:\n#request.httprequest.'
'headers.get("X-Github-Event")',
)
python_code_get_ip = fields.Text(
'Get IP',
required=True,
help='Python code to get remote IP address '
'from request data.\n'
'You have object.env.request variable with full '
'webhook request.',
default='# You can use object.env.request variable '
'to get full data of webhook request.\n'
'# Example:\n'
'#object.env.request.httprequest.remote_addr'
'\nrequest.httprequest.remote_addr',
)
active = fields.Boolean(default=True)
@api.multi
def process_python_code(self, python_code, request=None):
"""
Execute a python code with eval.
:param string python_code: Python code to process
:param object request: Request object with data of json
and http request
:return: Result of process python code.
"""
self.ensure_one()
res = None
eval_dict = {
'user': self.env.user,
'object': self,
'request': request,
# copy context to prevent side-effects of eval
'context': dict(self.env.context),
}
try:
res = safe_eval(python_code, eval_dict)
except BaseException:
error = tools.ustr(traceback.format_exc())
_logger.debug(
'python_code "%s" with dict [%s] error [%s]',
python_code, eval_dict, error)
if isinstance(res, basestring):
res = tools.ustr(res)
return res
@api.model
def search_with_request(self, request):
"""
Method to search of all webhook
and return only one that match with remote address
and range of address.
:param object request: Request object with data of json
and http request
:return: object of webhook found or
if not found then return False
"""
for webhook in self.search([('active', '=', True)]):
remote_address = webhook.process_python_code(
webhook.python_code_get_ip, request)
if not remote_address:
continue
if webhook.is_address_range(remote_address):
return webhook
return False
@api.multi
def is_address_range(self, remote_address):
"""
Check if a remote IP address is in range of one
IP or network address of current object data.
:param string remote_address: Remote IP address
:return: if remote address match then return True
else then return False
"""
self.ensure_one()
for address in self.address_ids:
ipn = ipaddress.ip_network(u'' + address.name)
hosts = [host.exploded for host in ipn.hosts()]
hosts.append(address.name)
if remote_address in hosts:
return True
return False
@api.model
def get_event_methods(self, event_method_base):
"""
List all methods of current object that start with base name.
e.g. if exists methods called 'x1', 'x2'
and other ones called 'y1', 'y2'
if you have event_method_base='x'
Then will return ['x1', 'x2']
:param string event_method_base: Name of method event base
returns: List of methods with that start wtih method base
"""
# TODO: Filter just callable attributes
return sorted(
attr for attr in dir(self) if attr.startswith(
event_method_base)
)
@api.model
def get_ping_events(self):
"""
List all name of event type ping.
This event is a dummy event just to
know if a provider is working.
:return: List with names of ping events
"""
return ['ping']
@api.multi
def run_webhook(self, request):
"""
Run methods to process a webhook event.
Get all methods with base name
'run_CONSUMER_EVENT*'
Invoke all methods found.
:param object request: Request object with data of json
and http request
:return: True
"""
self.ensure_one()
event = self.process_python_code(
self.python_code_get_event, request)
if not event:
raise exceptions.ValidationError(_(
'event is not defined'))
method_event_name_base = \
'run_' + self.name + \
'_' + event
methods_event_name = self.get_event_methods(method_event_name_base)
if not methods_event_name:
# if is a 'ping' event then return True
# because the request is received fine.
if event in self.get_ping_events():
return True
raise exceptions.ValidationError(_(
'Not defined methods "%s" yet' % (
method_event_name_base)))
self.env.request = request
for method_event_name in methods_event_name:
method = getattr(self, method_event_name)
res_method = method()
if isinstance(res_method, list) and len(res_method) == 1:
if res_method[0] is NotImplemented:
_logger.debug(
'Not implemented method "%s" yet', method_event_name)
return True

3
webhook/security/ir.model.access.csv

@ -0,0 +1,3 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_webhook_address,access_webhook_address,model_webhook_address,base.group_system,1,1,1,1
access_webhook,access_webhook,model_webhook,base.group_system,1,1,1,1

5
webhook/tests/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Vauxoo - https://www.vauxoo.com/
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_webhook_post

124
webhook/tests/test_webhook_post.py

@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Vauxoo - https://www.vauxoo.com/
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import json
import requests
from openerp.tests.common import HttpCase
from openerp import api, exceptions, tools, models
from openerp.tools.translate import _
HOST = '127.0.0.1'
PORT = tools.config['xmlrpc_port']
class Webhook(models.Model):
_inherit = 'webhook'
@api.multi
def run_wehook_test_get_foo(self):
"""
This method is just to test webhook.
This needs receive a json request with
next json values: {'foo': 'bar'}
If value is different will raise a error.
"""
self.ensure_one()
if self.env.request.jsonrequest['foo'] != 'bar':
raise exceptions.ValidationError(_("Wrong value received"))
class FakeHttpRequest(object):
remote_address = None
headers = {}
class FakeRequest(object):
def __init__(self, **args):
self.httprequest = FakeHttpRequest()
class TestWebhookPost(HttpCase):
def setUp(self):
super(TestWebhookPost, self).setUp()
self.webhook = self.env['webhook']
self.url_base = "http://%s:%s" % (HOST, PORT)
self.url = self.get_webhook_url()
def get_webhook_url(self, url='/webhook',
webhook_name="wehook_test"):
"""
:param string url: Full url of last url of webhook to use.
If you use a full url will return url
plus session_id
default: /webhook
:param string webhook_name: Name of webhook to process
default: webhook_test
:return: url with
http://IP:PORT/webhook/webhook_name?session_id=###
"""
webhook_name = webhook_name.replace('/', '')
if url.startswith('/'):
url = self.url_base + url + '/' + webhook_name
url += '?session_id=' + self.session_id
return url
def post_webhook_event(self, event, url, data, remote_ip=None,
headers=None, params=None):
"""
:param string event String: Name of webhook event.
:param string url: Full url of webhook services.
:param dict data: Payload data of request.
:param string remote_ip: Remote IP of webhook to set in
test variable.
:param dict headers: Request headers with main data.
:param dict params: Extra values to send to webhook.
"""
if headers is None:
headers = {}
if remote_ip is None:
remote_ip = '127.0.0.1'
headers.update({
'X-Webhook-Test-Event': event,
'X-Webhook-Test-Address': remote_ip,
})
headers.setdefault('accept', 'application/json')
headers.setdefault('content-type', 'application/json')
payload = json.dumps(data)
response = requests.request(
"POST", url, data=payload,
headers=headers, params=params)
return response.json()
def test_webhook_ping(self):
"""
Test to check that 'ping' generic method work fine!
'ping' event don't need to add it in inherit class.
"""
json_response = self.post_webhook_event(
'ping', self.url, {})
has_error = json_response.get('error', False)
self.assertEqual(
has_error, False, 'Error in webhook ping test!')
def test_webhook_get_foo(self):
"""
Test to check that 'get_foo' event from 'webhook_test'
work fine!
This event is defined in inherit method of test.
"""
json_response = self.post_webhook_event(
'get_foo', self.url, {'foo': 'bar'})
self.assertEqual(
json_response.get('error', False), False,
'Error in webhook get foo test!.')
def test_webhook_search_with_request(self):
"""Test to check that 'search_with_request' method works!"""
fake_req = FakeRequest()
fake_req.httprequest.headers['X-Webhook-Test-Address'] = '127.0.0.1'
wh = self.webhook.search_with_request(fake_req)
self.assertEqual(wh.id, self.env.ref('webhook.webhook_test').id,
"Search webhook from request IP info is not working")

44
webhook/views/webhook_views.xml

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_webhook_form" model="ir.ui.view">
<field name="name">view.webhook.form</field>
<field name="model">webhook</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<form>
<group>
<field name="name"/>
<field name="address_ids" widget="one2many_list">
<tree string="Address" editable="top">
<field name="name"/>
</tree>
</field>
<field name="python_code_get_event"/>
<field name="active"/>
</group>
</form>
</field>
</record>
<record id="view_webhook_tree" model="ir.ui.view">
<field name="name">view.webhook.tree</field>
<field name="model">webhook</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="active"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="action_webhook">
<field name="name">Webhook</field>
<field name="res_model">webhook</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="webhook_menu_action" name="Webhook" parent="base.menu_automation" action="action_webhook"/>
</data>
</openerp>
Loading…
Cancel
Save