Skip to content

Commit 0055fff

Browse files
committed
Add some helpers + CRUD & more restructuring
1 parent dd1216f commit 0055fff

4 files changed

Lines changed: 254 additions & 38 deletions

File tree

oauth_provider/controllers/oauth_api_controller.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,19 @@ def data_get(self, access_token=None, model=None, *args, **kwargs):
6868
operation.
6969
model (str): Name of model to operate on.
7070
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.
71+
standard Odoo format. This will be appended to the scope's
72+
pre-existing filter.
7473
"""
7574
token = self._get_token(access_token)
7675
model = self._get_model(model)
77-
data = token.get_data_for_model(
76+
data = token.get_data(
7877
model,
7978
domain=kwargs.get('domain', []),
8079
)
8180
return self._json_response(data=data)
8281

8382
@route('/oauth2/data',
84-
type='oauth',
83+
type='json',
8584
auth='none',
8685
csrf=False,
8786
methods=['POST'],

oauth_provider/exceptions.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,28 @@
44

55
from werkzeug import exceptions
66

7+
from odoo import _
8+
79

810
class OauthApiException(exceptions.BadRequest):
911
pass
1012

1113

1214
class OauthInvalidTokenException(exceptions.Unauthorized):
15+
1316
def __init__(self):
1417
super(OauthInvalidTokenException, self).__init__(
15-
'Invalid or Expired Token',
18+
_('Invalid or expired token'),
1619
)
1720
self.traceback = ('', '', '')
1821

1922

23+
class OauthScopeValidationException(exceptions.Forbidden):
24+
25+
def __init__(self, code='unknown'):
26+
msg = _(
27+
'There was an error validating the attempted action against '
28+
'your session\'s authorized scope. The error code is: %s',
29+
)
30+
super(OauthInvalidTokenException, self).__init__(msg % code)
31+
self.traceback = ('', '', '')

oauth_provider/models/oauth_provider_scope.py

Lines changed: 211 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
import dateutil
77
import time
88
from collections import defaultdict
9-
from odoo import models, api, fields
9+
10+
from odoo import api, fields, models, _
1011
from odoo.tools.safe_eval import safe_eval
1112

13+
from ..exceptions import OauthScopeValidationException
14+
1215

1316
class OauthProviderScope(models.Model):
1417
_name = 'oauth.provider.scope'
@@ -41,9 +44,10 @@ class OauthProviderScope(models.Model):
4144
('code_unique', 'UNIQUE (code)',
4245
'The code of the scopes must be unique !'),
4346
]
44-
47+
48+
@property
4549
@api.model
46-
def _get_ir_filter_eval_context(self):
50+
def ir_filter_eval_context(self):
4751
""" Returns the base eval context for ir.filter domains evaluation """
4852
return {
4953
'datetime': datetime,
@@ -54,23 +58,46 @@ def _get_ir_filter_eval_context(self):
5458
}
5559

5660
@api.multi
57-
def get_data_for_model(self, model, res_id=None, all_scopes_match=False):
58-
""" Return the data matching the scopes from the requested model """
61+
def filter_by_model(self, model):
62+
""" Return the current scopes that are associated to the model.
63+
64+
Args:
65+
model (str): Name of the model to operate on.
66+
67+
Returns:
68+
OauthProviderScope: Recordsets associated to the model.
69+
"""
70+
return self.filtered(lambda record: record.model == model)
71+
72+
@api.multi
73+
def get_data(self, model, res_id=None, all_scopes_match=False,
74+
domain=None):
75+
""" Return the data matching the scopes from the requested model.
76+
77+
Args:
78+
model (str): Name of the model to operate on.
79+
res_id (int): ID of record to find. Will only return this record,
80+
if defined.
81+
all_scopes_match (bool): True to filter out records that do not
82+
match all of the scopes in the current recordset.
83+
domain (list of tuples, optional): Domain to append to the
84+
`filter_domain` that is defined in the scope.
85+
86+
Returns:
87+
dict: If `res_id` is defined, this will be the scoped data for the
88+
appropriate record (or empty dict if no match). Otherwise,
89+
this will be a dictionary of scoped record data, keyed by
90+
record ID.
91+
"""
92+
5993
data = defaultdict(dict)
60-
eval_context = self._get_ir_filter_eval_context()
94+
eval_context = self.ir_filter_eval_context
6195
all_scopes_records = self.env[model]
62-
for scope in self.filtered(lambda record: record.model == model):
63-
# Retrieve the scope's domain
64-
filter_domain = [(1, '=', 1)]
65-
if scope.filter_id:
66-
filter_domain = safe_eval(
67-
scope.filter_id.sudo().domain, eval_context)
68-
if res_id is not None:
69-
filter_domain.append(('id', '=', res_id))
70-
71-
# Retrieve data of the matching records, depending on the scope's
72-
# fields
73-
records = self.env[model].search(filter_domain)
96+
97+
for scope in self.filter_by_model(model):
98+
99+
records = scope._get_scoped_records(eval_context, domain)
100+
74101
for record_data in records.read(scope.field_ids.mapped('name')):
75102
for field, value in record_data.items():
76103
if isinstance(value, tuple):
@@ -86,17 +113,174 @@ def get_data_for_model(self, model, res_id=None, all_scopes_match=False):
86113
all_scopes_records &= records
87114

88115
# If all scopes are required to match, filter the results to keep only
89-
# those mathing all scopes
116+
# those matching all scopes
90117
if all_scopes_match:
91-
data = dict(filter(
92-
lambda record_data: record_data[0] in all_scopes_records.ids,
93-
data.items()))
94-
95-
# If a single record was requested, return only data coming from this
96-
# record
97-
# Return an empty dictionnary if this record didn't recieve data to
98-
# return
118+
data = dict(
119+
filter(
120+
lambda _data: _data[0] in all_scopes_records.ids,
121+
data.items(),
122+
),
123+
)
124+
99125
if res_id is not None:
100126
data = data.get(res_id, {})
101127

102128
return data
129+
130+
@api.multi
131+
def create_record(self, model, vals):
132+
""" Create a record, validate the scope, and return (if valid).
133+
134+
Args:
135+
model (str): Name of the model to operate on.
136+
vals (dict): Values to create record with, keyed by field name.
137+
138+
Returns:
139+
OauthProviderScope: Newly created record
140+
141+
Raises:
142+
OauthScopeValidationException: If fields are included in vals,
143+
but are not within the current scope.
144+
"""
145+
146+
if not self._validate_scope_field(model, vals):
147+
raise OauthScopeValidationException('field')
148+
149+
record = self.env[model].create(vals)
150+
151+
if not self._validate_scope_record(record):
152+
raise OauthScopeValidationException('record')
153+
154+
return record
155+
156+
@api.multi
157+
def write_record(self, records, vals):
158+
""" Write to a recordset, adhering to the current scope.
159+
160+
Args:
161+
records (models.Model): Recordset to write to.
162+
vals (dict): Values to modify records with, keyed by field name.
163+
164+
Returns:
165+
OauthProviderScope: The same recordset that was provided as input.
166+
167+
Raises:
168+
OauthScopeValidationException: Raised in the following cases:
169+
* If records are attempted to be edited, but are not within
170+
the current scope.
171+
* If fields are included in vals, but are not within the
172+
current scope.
173+
* If the record no longer falls within scope after being
174+
"""
175+
176+
if not self._validate_scope_field(records._name, vals):
177+
raise OauthScopeValidationException('field')
178+
179+
if not self._validate_scope_record(records):
180+
raise OauthScopeValidationException('record')
181+
182+
records.write(vals)
183+
184+
if not self._validate_scope_record(records):
185+
raise OauthScopeValidationException('record')
186+
187+
return records
188+
189+
@api.multi
190+
def delete_record(self, records):
191+
""" Delete a recordset that is within the current scope.
192+
193+
Args:
194+
records (models.Model): Recordset to delete.
195+
196+
Raises:
197+
OauthScopeValidationException: If records are not within the
198+
current scope.
199+
"""
200+
201+
if not self._validate_scope_record(records):
202+
raise OauthScopeValidationException('record')
203+
204+
records.unlink()
205+
206+
@api.multi
207+
def _get_filter_domain(self, eval_context):
208+
""" Return the scope's domain.
209+
210+
Args:
211+
eval_context (dict): Base eval context, such as provided by
212+
`ir_filter_eval_context`
213+
214+
Returns:
215+
list of tuples: Domain of the scope, in standard Odoo format.
216+
"""
217+
self.ensure_one()
218+
filter_domain = [(1, '=', 1)]
219+
if self.filter_id:
220+
filter_domain = safe_eval(
221+
self.filter_id.sudo().domain,
222+
eval_context,
223+
)
224+
if res_id is not None:
225+
filter_domain.append(
226+
('id', '=', res_id),
227+
)
228+
return filter_domain
229+
230+
@api.multi
231+
def _get_scoped_records(self, model, eval_context=None, add_domain=None):
232+
""" Return records that are within the scopes in the recordset.
233+
234+
Args:
235+
model (str): Name of the model to operate on.
236+
eval_context (dict, optional): Base eval context, such as provided
237+
by `ir_filter_eval_context`.
238+
add_domain (list of tuples, optional): Domain to append to the
239+
`filter_domain` that is defined in the scope.
240+
241+
Returns:
242+
models.Model: Recordset matching the scope.
243+
"""
244+
self.ensure_one()
245+
if eval_context is None:
246+
eval_context = self.ir_filter_eval_context
247+
if add_domain is None:
248+
add_domain = []
249+
filter_domain = self._get_filter_domain(eval_context)
250+
return self.env[model].search(filter_domain + add_domain)
251+
252+
@api.multi
253+
def _validate_scope_record(self, records):
254+
""" Validate that the recordset is within the current scope.
255+
256+
Args:
257+
records (models.Model): Recordset to validate.
258+
259+
Returns:
260+
bool: Indicating whether the records are within scope.
261+
"""
262+
263+
scoped_records = self._get_scoped_records(
264+
self.env[records._name],
265+
)
266+
return all([
267+
r.id in scoped_records for r in records
268+
])
269+
270+
@api.multi
271+
def _validate_scope_field(self, model, vals):
272+
""" Validate that the input vals do not violate the current scope.
273+
274+
Args:
275+
model (str): Name of the model to operate on.
276+
vals (dict): Values that should be checked against the current
277+
scope, keyed by field name.
278+
279+
Returns:
280+
bool: Whether the values are within the scope.
281+
"""
282+
scopes = self.filter_by_model(model)
283+
field_names = scopes.field_ids.mapped('name')
284+
return all([
285+
f in field_names for f in vals.keys()
286+
])

oauth_provider/models/oauth_provider_token.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ class OauthProviderToken(models.Model):
3939
'The refresh token must be unique per client !'),
4040
]
4141

42+
@property
43+
@api.multi
44+
def user_scopes(self):
45+
self.ensure_one()
46+
return self.sudo(user=self.user_id).scope_ids
47+
4248
@api.multi
4349
def _compute_active(self):
4450
for token in self:
@@ -85,15 +91,30 @@ def generate_user_id(self):
8591
return self.client_id.generate_user_id(self.user_id)
8692

8793
@api.multi
88-
def get_data_for_model(self, model, res_id=None, all_scopes_match=False):
94+
def get_data(self, model, res_id=None, all_scopes_match=False,
95+
domain=None):
8996
""" Returns the data of the accessible records of the requested model,
9097
98+
Args:
99+
model (str): Name of the model to operate on.
100+
res_id (int): ID of record to find. Will only return this record,
101+
if defined.
102+
all_scopes_match (bool): True to filter out records that do not
103+
match all of the scopes in the current recordset.
104+
domain (list of tuples, optional): Domain to append to the
105+
`filter_domain` that is defined in the scope.
106+
107+
Returns:
108+
dict: If `res_id` is defined, this will be the scoped data for the
109+
appropriate record (or empty dict if no match). Otherwise,
110+
this will be a dictionary of scoped record data, keyed by
111+
record ID.
112+
91113
Data are returned depending on the allowed scopes for the token
92114
If the all_scopes_match argument is set to True, return only records
93115
allowed by all token's scopes
94116
"""
95-
self.ensure_one()
96-
97117
# Retrieve records allowed from all scopes
98-
return self.sudo(user=self.user_id).scope_ids.get_data_for_model(
99-
model, res_id=res_id, all_scopes_match=all_scopes_match)
118+
return self.user_scopes.get_data(
119+
model, res_id=res_id, all_scopes_match=all_scopes_match,
120+
)

0 commit comments

Comments
 (0)