Skip to content

Commit 7a6d879

Browse files
committed
Add token adapter/interface mechanism
1 parent 2839174 commit 7a6d879

File tree

9 files changed

+195
-38
lines changed

9 files changed

+195
-38
lines changed

base_web_hook/controllers/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ class WebHookController(http.Controller):
1414
)
1515
def receive(self, slug, **kwargs):
1616
hook = self.env['web.hook'].search_by_slug(slug)
17-
return hook.receive(kwargs)
17+
return hook.receive(kwargs, http.request.httprequest.get_data())

base_web_hook/models/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33

44
from . import web_hook
55
from . import web_hook_adapter
6-
from . import web_hook_generic
6+
from . import web_hook_token
7+
from . import web_hook_token_adapter
8+
from . import web_hook_token_plain

base_web_hook/models/web_hook.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
import logging
66

7-
from odoo import api, fields, models
7+
from werkzeug.exceptions import Unauthorized
8+
9+
from odoo import api, fields, models, _
810

911
_logger = logging.getLogger(__name__)
1012

@@ -37,6 +39,10 @@ class WebHook(models.Model):
3739
help='This is the URI that is used to call the web hook externally.',
3840
compute='_compute_uri',
3941
)
42+
token_id = fields.Many2one(
43+
string='Token',
44+
comodel_name='web.hook.token',
45+
)
4046

4147
@api.model
4248
def _get_interface_types(self):
@@ -72,20 +78,30 @@ def search_by_slug(self, slug):
7278
return self.browse(record_id)
7379

7480
@api.multi
75-
def receive(self, data=None):
81+
def receive(self, data=None, data_string=None):
7682
"""This method is used to receive a web hook.
7783
78-
It simply passes the received data to the underlying interface's
79-
``receive`` method for processing, and returns the result. The
80-
result returned by the interface must be JSON serializable.
84+
First it extracts the token, then validates using ``token.validate``
85+
and raises as ``Unauthorized`` if it is invalid. It then passes the
86+
received data to the underlying interface's ``receive`` method for
87+
processing, and returns the result. The result returned by the
88+
interface must be JSON serializable.
8189
8290
Args:
83-
data (dict, optional): Data to pass to the hook's ``receive``
84-
method.
91+
data (dict, optional): Parsed data that was received in the
92+
request.
93+
data_string (str, optional): The raw data that was received in the
94+
request body.
8595
8696
Returns:
8797
mixed: A JSON serializable return from the interface's
8898
``receive`` method.
8999
"""
90100
self.ensure_one()
91-
return self.interface.receive()
101+
token = self.interface.extract_token(data)
102+
if not self.token_id.validate(token, data, data_string):
103+
raise Unauthorized(_(
104+
'The request could not be processed: '
105+
'An invalid token was received.'
106+
))
107+
return self.interface.receive(data)

base_web_hook/models/web_hook_adapter.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,16 @@ def receive(self, data=None):
3232
mixed: A JSON serializable return, or ``None``.
3333
"""
3434
raise NotImplementedError()
35+
36+
@api.multi
37+
def extract_token(self, data=None):
38+
"""Extract the token from the data and return it.
39+
40+
Args:
41+
data (dict, optional): Data that was received with the hook.
42+
43+
Returns:
44+
mixed: The token data. Should be compatible with the hook's token
45+
interface (the ``token`` parameter of ``token_id.validate``).
46+
"""
47+
raise NotImplementedError()

base_web_hook/models/web_hook_generic.py

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 LasLabs Inc.
3+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
4+
5+
import random
6+
import string
7+
8+
from odoo import api, fields, models
9+
10+
11+
class WebHookToken(models.Model):
12+
"""This represents a generic token for use in a secure web hook exchange.
13+
14+
It serves as an interface for token adapters. No logic should need to be
15+
added to this model in inherited modules.
16+
"""
17+
18+
_name = 'web.hook.token'
19+
_description = 'Web Hook Token'
20+
21+
hook_id = fields.Many2one(
22+
string='Hook',
23+
comodel_name='web.hook',
24+
required=True,
25+
ondelete='cascade',
26+
)
27+
token = fields.Reference(
28+
selection='_get_token_types',
29+
readonly=True,
30+
help='This is the token used for hook authentication. It is '
31+
'created automatically upon creation of the web hook, and '
32+
'is also deleted with it.',
33+
)
34+
token_type = fields.Selection(
35+
selection='_get_token_types',
36+
required=True,
37+
)
38+
secret = fields.Char(
39+
help='This is the secret that is configured for the token exchange. '
40+
'This configuration is typically performed when setting the '
41+
'token up in the remote system. For ease, a secure random value '
42+
'has been provided as a default.',
43+
default=lambda s: s._default_secret(),
44+
)
45+
46+
@api.model
47+
def _get_token_types(self):
48+
"""Return the web hook token interface models that are installed."""
49+
adapter = self.env['web.hook.token.adapter']
50+
return [
51+
(m, self.env[m]._description) for m in adapter._inherit_children
52+
]
53+
54+
@api.model
55+
def _default_secret(self, length=254):
56+
characters = string.printable.split()[0]
57+
return ''.join(
58+
random.choice(characters) for _ in range(length)
59+
)
60+
61+
@api.multi
62+
def validate(self, token, data=None, data_string=None):
63+
"""This method is used to validate a web hook.
64+
65+
It simply passes the received data to the underlying token's
66+
``validate`` method for processing, and returns the result.
67+
68+
Args:
69+
token (mixed): The "secure" token string that should be validated
70+
against the dataset. Typically a string.
71+
data (dict, optional): Parsed data that was received with the
72+
request.
73+
data_string (str, optional): Raw form data that was received in
74+
the request. This is useful for computation of hashes, because
75+
Python dictionaries do not maintain sort order and thus are
76+
useless for crypto.
77+
78+
Returns:
79+
bool: If the token is valid or not.
80+
"""
81+
self.ensure_one()
82+
return self.token.validate(token, data, data_string)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 LasLabs Inc.
3+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
4+
5+
from odoo import api, fields, models
6+
7+
8+
class WebHookTokenAdapter(models.AbstractModel):
9+
"""This should be inherited by all token interfaces."""
10+
11+
_name = 'web.hook.token.adapter'
12+
_description = 'Web Hook Token Adapter'
13+
14+
token_id = fields.Many2one(
15+
string='Token',
16+
comodel_name='web.hook.token',
17+
required=True,
18+
ondelete='cascade',
19+
)
20+
21+
@api.multi
22+
def validate(self, token_string, data, data_string):
23+
"""Return ``True`` if the token is valid. Otherwise, ``False``.
24+
25+
Child models should inherit this method to provide token validation
26+
logic.
27+
28+
Args:
29+
token_string (str): The "secure" token string that should be
30+
validated against the dataset.
31+
data (dict, optional): Parsed data that was received with the
32+
request.
33+
data_string (str, optional): Raw form data that was received in
34+
the request. This is useful for computation of hashes, because
35+
Python dictionaries do not maintain sort order and thus are
36+
useless for crypto.
37+
38+
Returns:
39+
bool: If the token is valid or not.
40+
"""
41+
raise NotImplementedError()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 LasLabs Inc.
3+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
4+
5+
from odoo import api, fields, models
6+
7+
8+
class WebHookTokenPlain(models.Model):
9+
"""This is a plain text token."""
10+
11+
_name = 'web.hook.token.plain'
12+
_inherit = 'web.hook.token.adapter'
13+
_description = 'Web Hook Token - Plain'
14+
15+
@api.multi
16+
def validate(self, token_string, _, _):
17+
"""Return ``True`` if the received token is the same as configured.
18+
"""
19+
return token_string == self.token_id.secret

base_web_hook/models/website.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 LasLabs Inc.
3+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
4+
5+
from odoo import api, fields, models
6+
7+
8+
class Website(models.Model):
9+
_inherit = 'website'
10+
11+
12+

0 commit comments

Comments
 (0)