Skip to content

Commit 3fbf9f8

Browse files
committed
Start
1 parent 91a367d commit 3fbf9f8

23 files changed

Lines changed: 458 additions & 123 deletions

oauth_provider/README.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ For example, to configure an Odoo's *auth_oauth* module compatible client, you w
6868
Usage
6969
=====
7070

71+
Server
72+
------
73+
7174
This module will allow OAuth clients to use your Odoo instance as an OAuth provider.
7275

7376
Once configured, you must give these information to your client application :
@@ -82,7 +85,7 @@ Once configured, you must give these information to your client application :
8285
Parameters : access_token
8386
- User information request : http://odoo.example.com/oauth2/userinfo
8487
Parameters : access_token
85-
- Any other model information request (depending on the scopes) : http://odoo.example.com/oauth2/otherinfo
88+
- Any other model information request (depending on the scopes) : http://odoo.example.com/oauth2/data
8689
Parameters : access_token and model
8790

8891
For example, to configure the *auth_oauth* Odoo module as a client, you will enter these values :
@@ -95,6 +98,7 @@ For example, to configure the *auth_oauth* Odoo module as a client, you will ent
9598
- Validation URL : http://odoo.example.com/oauth2/tokeninfo
9699
- Data URL : http://odoo.example.com/oauth2/userinfo
97100

101+
98102
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
99103
:alt: Try me on Runbot
100104
:target: https://runbot.odoo-community.org/runbot/149/10.0

oauth_provider/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
'version': '10.0.1.0.0',
99
'category': 'Authentication',
1010
'website': 'http://www.syleam.fr/',
11-
'author': 'SYLEAM, Odoo Community Association (OCA)',
11+
'author': 'SYLEAM, LasLabs, Odoo Community Association (OCA)',
1212
'license': 'AGPL-3',
1313
'installable': True,
1414
'external_dependancies': {

oauth_provider/controllers/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22
# Copyright 2016 SYLEAM
33
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
44

5-
from . import main
5+
from . import oauth_mixin
6+
7+
from . import oauth_api_controller
8+
from . import oauth_provider_controller
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 SYLEAM
3+
# Copyright 2017 LasLabs Inc.
4+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
5+
6+
import logging
7+
8+
from odoo import http, fields
9+
from odoo.addons.web.controllers.main import ensure_db
10+
11+
from ..http import route
12+
from ..exceptions import OauthApiException, OauthInvalidTokenException
13+
from .oauth2_mixin import OauthMixin
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
18+
class OauthApiController(OauthMixin):
19+
20+
def _get_model(self, model_name):
21+
""" Validate the access token & return a usable model.
22+
23+
Args:
24+
model_name (str): Name of model to find.
25+
26+
Returns:
27+
IrModel: Usable model object that matched model_name.
28+
29+
Raises:
30+
OauthApiException: If the model is not found.
31+
"""
32+
ensure_db()
33+
model_obj = http.request.env['ir.model'].search([
34+
('model', '=', model_name),
35+
])
36+
if not model_obj:
37+
raise OauthApiException('Model Not Found')
38+
return model_obj
39+
40+
def _get_token(self, access_token):
41+
""" Find the access token & return it if valid.
42+
43+
Args:
44+
access_token (str): Access token that should be validated.
45+
46+
Returns:
47+
OAuthProviderToken: Token record, if valid.
48+
49+
Raises:
50+
OauthInvalidTokenException: When the token is invalid or expired.
51+
"""
52+
ensure_db()
53+
token = self._get_access_token(access_token)
54+
if not token:
55+
raise OauthInvalidTokenException()
56+
return token
57+
58+
@route('/oauth2/data',
59+
type='oauth',
60+
auth='none',
61+
methods=['GET'],
62+
)
63+
def data_get(self, access_token=None, model=None, *args, **kwargs):
64+
""" Return allowed information about the requested model.
65+
66+
Args:
67+
access_token (str): OAuth2 access token to utilize for the
68+
operation.
69+
model (str): Name of model to operate on.
70+
domain (list, optional): Domain to apply to the search, in the
71+
standard Odoo format. This only applies if there is not
72+
already a filter on the token scope. Otherwise, the scope
73+
will take precedence.
74+
"""
75+
token = self._get_token(access_token)
76+
model = self._get_model(model)
77+
data = token.get_data_for_model(
78+
model,
79+
domain=kwargs.get('domain', []),
80+
)
81+
return self._json_response(data=data)
82+
83+
@route('/oauth2/data',
84+
type='oauth',
85+
auth='none',
86+
csrf=False,
87+
methods=['POST'],
88+
)
89+
def data_post(self, access_token=None, model=None, record_ids=None,
90+
*args, **kwargs):
91+
""" Update the records """
92+
token = self._get_token(access_token)
93+
model = self._get_model(model)
94+
raise OauthInvalidTokenException()
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 SYLEAM
3+
# Copyright 2017 LasLabs Inc.
4+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
5+
6+
import json
7+
import logging
8+
import werkzeug.wrappers
9+
10+
from odoo import http
11+
from odoo.addons.web.controllers.main import ensure_db
12+
13+
from ..http import OauthRequest
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
try:
18+
import oauthlib
19+
from oauthlib import oauth2
20+
except ImportError:
21+
_logger.debug('Cannot `import oauthlib`.')
22+
23+
24+
class OauthMixin(http.Controller):
25+
26+
@classmethod
27+
def _get_access_token(cls, access_token):
28+
""" Verify access token and return record if valid.
29+
30+
Args:
31+
access_token (str): OAuth2 access token to be validated.
32+
33+
Returns:
34+
OauthProviderToken: Valid token record for use.
35+
NoneType: None if no matching token was found in the database.
36+
bool: False if the token was invalid.
37+
"""
38+
token = http.request.env['oauth.provider.token'].search([
39+
('token', '=', access_token),
40+
])
41+
if not token:
42+
return None
43+
44+
oauth2_server = token.client_id.get_oauth2_server()
45+
# Retrieve needed arguments for oauthlib methods
46+
uri, http_method, body, headers = cls._get_request_information()
47+
48+
# Validate request information
49+
valid, oauthlib_request = oauth2_server.verify_request(
50+
uri, http_method=http_method, body=body, headers=headers,
51+
)
52+
53+
if valid:
54+
return token
55+
56+
return False
57+
58+
@staticmethod
59+
def _get_request_information():
60+
""" Retrieve needed arguments for oauthlib methods.
61+
62+
Returns:
63+
tuple: uri, http_method, body, headers
64+
"""
65+
uri = http.request.httprequest.base_url
66+
http_method = http.request.httprequest.method
67+
body = oauthlib.common.urlencode(
68+
http.request.httprequest.values.items(),
69+
)
70+
headers = http.request.httprequest.headers
71+
72+
return uri, http_method, body, headers
73+
74+
@staticmethod
75+
def _json_response(data=None, status=200, headers=None):
76+
""" Returns a json response to the client.
77+
78+
Args:
79+
data (mixed, optional): Response data to JSON encode.
80+
status (int, optional): HTTP status code to respond with.
81+
headers (dict, optional): Mapping of headers to apply to the request. If
82+
the `Content-Type` header is not defined, `application/json`
83+
will automatically be added.
84+
85+
Returns:
86+
BaseResponse: Werkzeug response object based on the input.
87+
"""
88+
return OauthRequest._json_response(data, headers=headers)

oauth_provider/controllers/main.py renamed to oauth_provider/controllers/oauth_provider_controller.py

Lines changed: 43 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
from odoo import http, fields
1111
from odoo.addons.web.controllers.main import ensure_db
1212

13+
from ..http import route
14+
from .oauth2_mixin import OauthMixin
15+
1316
_logger = logging.getLogger(__name__)
1417

1518
try:
@@ -19,50 +22,13 @@
1922
_logger.debug('Cannot `import oauthlib`.')
2023

2124

22-
class OAuth2ProviderController(http.Controller):
23-
def __init__(self):
24-
super(OAuth2ProviderController, self).__init__()
25-
26-
def _get_request_information(self):
27-
""" Retrieve needed arguments for oauthlib methods """
28-
uri = http.request.httprequest.base_url
29-
http_method = http.request.httprequest.method
30-
body = oauthlib.common.urlencode(
31-
http.request.httprequest.values.items())
32-
headers = http.request.httprequest.headers
33-
34-
return uri, http_method, body, headers
35-
36-
def _check_access_token(self, access_token):
37-
""" Check if the provided access token is valid """
38-
token = http.request.env['oauth.provider.token'].search([
39-
('token', '=', access_token),
40-
])
41-
if not token:
42-
return False
43-
44-
oauth2_server = token.client_id.get_oauth2_server()
45-
# Retrieve needed arguments for oauthlib methods
46-
uri, http_method, body, headers = self._get_request_information()
47-
48-
# Validate request information
49-
valid, oauthlib_request = oauth2_server.verify_request(
50-
uri, http_method=http_method, body=body, headers=headers)
51-
52-
if valid:
53-
return token
54-
55-
return False
56-
57-
def _json_response(self, data=None, status=200, headers=None):
58-
""" Returns a json response to the client """
59-
if headers is None:
60-
headers = {'Content-Type': 'application/json'}
61-
62-
return werkzeug.wrappers.BaseResponse(
63-
json.dumps(data), status=status, headers=headers)
25+
class OAuth2ProviderController(OauthMixin):
6426

65-
@http.route('/oauth2/authorize', type='http', auth='user', methods=['GET'])
27+
@route('/oauth2/authorize',
28+
type='http',
29+
auth='user',
30+
methods=['GET'],
31+
)
6632
def authorize(self, client_id=None, response_type=None, redirect_uri=None,
6733
scope=None, state=None, *args, **kwargs):
6834
""" Check client's request, and display an authorization page to the user,
@@ -122,8 +88,11 @@ def authorize(self, client_id=None, response_type=None, redirect_uri=None,
12288
'oauth_scopes': oauth_scopes,
12389
})
12490

125-
@http.route(
126-
'/oauth2/authorize', type='http', auth='user', methods=['POST'])
91+
@http.route('/oauth2/authorize',
92+
type='http',
93+
auth='user',
94+
methods=['POST'],
95+
)
12796
def authorize_post(self, *args, **kwargs):
12897
""" Redirect to the requested URI during the authorization """
12998
client = http.request.env['oauth.provider.client'].search([
@@ -147,8 +116,12 @@ def authorize_post(self, *args, **kwargs):
147116

148117
return werkzeug.utils.redirect(headers['Location'], code=status)
149118

150-
@http.route('/oauth2/token', type='http', auth='none', methods=['POST'],
151-
csrf=False)
119+
@http.route('/oauth2/token',
120+
type='http',
121+
auth='none',
122+
methods=['POST'],
123+
csrf=False,
124+
)
152125
def token(self, client_id=None, client_secret=None, redirect_uri=None,
153126
scope=None, code=None, grant_type=None, username=None,
154127
password=None, refresh_token=None, *args, **kwargs):
@@ -199,14 +172,18 @@ def token(self, client_id=None, client_secret=None, redirect_uri=None,
199172
return werkzeug.wrappers.BaseResponse(
200173
body, status=status, headers=headers)
201174

202-
@http.route('/oauth2/tokeninfo', type='http', auth='none', methods=['GET'])
175+
@http.route('/oauth2/tokeninfo',
176+
type='http',
177+
auth='none',
178+
methods=['GET'],
179+
)
203180
def tokeninfo(self, access_token=None, *args, **kwargs):
204181
""" Return some information about the supplied token
205182
206183
Similar to Google's "tokeninfo" request
207184
"""
208185
ensure_db()
209-
token = self._check_access_token(access_token)
186+
token = self._get_access_token(access_token)
210187
if not token:
211188
return self._json_response(
212189
data={'error': 'invalid_or_expired_token'}, status=401)
@@ -228,26 +205,34 @@ def tokeninfo(self, access_token=None, *args, **kwargs):
228205
data.update(user_id=token.generate_user_id())
229206
return self._json_response(data=data)
230207

231-
@http.route('/oauth2/userinfo', type='http', auth='none', methods=['GET'])
208+
@http.route('/oauth2/userinfo',
209+
type='http',
210+
auth='none',
211+
methods=['GET'],
212+
)
232213
def userinfo(self, access_token=None, *args, **kwargs):
233214
""" Return some information about the user linked to the supplied token
234215
235216
Similar to Google's "userinfo" request
236217
"""
237218
ensure_db()
238-
token = self._check_access_token(access_token)
219+
token = self._get_access_token(access_token)
239220
if not token:
240221
return self._json_response(
241222
data={'error': 'invalid_or_expired_token'}, status=401)
242223

243224
data = token.get_data_for_model('res.users', res_id=token.user_id.id)
244225
return self._json_response(data=data)
245226

246-
@http.route('/oauth2/otherinfo', type='http', auth='none', methods=['GET'])
227+
@http.route('/oauth2/otherinfo',
228+
type='http',
229+
auth='none',
230+
methods=['GET'],
231+
)
247232
def otherinfo(self, access_token=None, model=None, *args, **kwargs):
248233
""" Return allowed information about the requested model """
249234
ensure_db()
250-
token = self._check_access_token(access_token)
235+
token = self._get_access_token(access_token)
251236
if not token:
252237
return self._json_response(
253238
data={'error': 'invalid_or_expired_token'}, status=401)
@@ -262,8 +247,11 @@ def otherinfo(self, access_token=None, model=None, *args, **kwargs):
262247
data = token.get_data_for_model(model)
263248
return self._json_response(data=data)
264249

265-
@http.route(
266-
'/oauth2/revoke_token', type='http', auth='none', methods=['POST'])
250+
@http.route('/oauth2/revoke_token',
251+
type='http',
252+
auth='none',
253+
methods=['POST'],
254+
)
267255
def revoke_token(self, token=None, *args, **kwargs):
268256
""" Revoke the supplied token """
269257
ensure_db()

0 commit comments

Comments
 (0)