@@ -1,3 +1,4 @@
|
||||
* Add size limit for RPC arguments
|
||||
* Store the ignore search limit option of export
|
||||
* Make field_names optional in Model.default_get
|
||||
* Remove check_xml_record from ModelStorage
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
RPC
|
||||
===
|
||||
|
||||
.. class:: RPC([readonly[, instantiate[, [decorator, result[, check_access[, unique[, fresh_session[, cache[, timeout]]]]]]]]])
|
||||
.. class:: RPC([readonly[, instantiate[, [decorator, result[, check_access[, unique[, fresh_session[, cache[, timeout[, size_limits]]]]]]]]]])
|
||||
|
||||
Define the behavior of Remote Procedure Call.
|
||||
|
||||
@@ -52,6 +52,11 @@ Instance attributes are:
|
||||
``0`` or ``None`` means no timeout is applied.
|
||||
Default is ``None``.
|
||||
|
||||
.. attribute:: RPC.size_limits
|
||||
|
||||
A dictionary with size limits for the arguments.
|
||||
The key of the dictionary define the position of the argument when it is an
|
||||
integer, or a slice of arguments when it is a tuple.
|
||||
|
||||
RPCCache
|
||||
--------
|
||||
|
||||
@@ -337,6 +337,15 @@ depending on the parameters.
|
||||
|
||||
Default: ``60``
|
||||
|
||||
.. _config-request.records_limit:
|
||||
|
||||
records_limit
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The maximal number of records processed by requests.
|
||||
|
||||
Default: ``None``
|
||||
|
||||
.. _config-cache:
|
||||
|
||||
cache
|
||||
|
||||
@@ -14,6 +14,9 @@ from trytond.rpc import RPC
|
||||
from trytond.tools import slugify
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
_request_timeout = config.getint('request', 'timeout', default=0)
|
||||
_request_records_limit = config.getint('request', 'records_limit')
|
||||
|
||||
|
||||
class DomainError(ValidationError):
|
||||
pass
|
||||
@@ -90,8 +93,16 @@ class DictSchemaMixin(object):
|
||||
def __setup__(cls):
|
||||
super(DictSchemaMixin, cls).__setup__()
|
||||
cls.__rpc__.update({
|
||||
'get_keys': RPC(instantiate=0),
|
||||
'search_get_keys': RPC(),
|
||||
'get_keys': RPC(
|
||||
instantiate=0,
|
||||
size_limits={
|
||||
0: _request_records_limit,
|
||||
}),
|
||||
'search_get_keys': RPC(
|
||||
size_limits={
|
||||
1: _request_records_limit,
|
||||
},
|
||||
timeout=_request_timeout),
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -56,7 +56,11 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
|
||||
cls.__string__ = humanize(cls.__name__)
|
||||
cls.__rpc__ = {
|
||||
'default_get': RPC(cache=dict(seconds=5 * 60)),
|
||||
'fields_get': RPC(cache=dict(days=1)),
|
||||
'fields_get': RPC(
|
||||
cache=dict(days=1),
|
||||
size_limits={
|
||||
1: 0,
|
||||
}),
|
||||
'pre_validate': RPC(instantiate=0),
|
||||
}
|
||||
cls.__access__ = set()
|
||||
|
||||
@@ -34,6 +34,8 @@ from .modelstorage import (
|
||||
ValidationError, is_leaf)
|
||||
from .modelview import ModelView
|
||||
|
||||
_request_records_limit = config.getint('request', 'records_limit')
|
||||
|
||||
|
||||
class ForeignKeyError(ValidationError):
|
||||
pass
|
||||
@@ -352,7 +354,10 @@ class ModelSQL(ModelStorage):
|
||||
cls._order = [('id', None)]
|
||||
if issubclass(cls, ModelView):
|
||||
cls.__rpc__.update({
|
||||
'history_revisions': RPC(),
|
||||
'history_revisions': RPC(
|
||||
size_limits={
|
||||
0: _request_records_limit,
|
||||
}),
|
||||
})
|
||||
if cls._history:
|
||||
history_table = cls.__table_history__()
|
||||
|
||||
@@ -41,6 +41,7 @@ _cache_count_timeout = config.getint(
|
||||
_cache_count_clear = config.getint(
|
||||
'cache', 'count_clear', default=1000)
|
||||
_request_timeout = config.getint('request', 'timeout', default=0)
|
||||
_request_records_limit = config.getint('request', 'records_limit')
|
||||
|
||||
|
||||
def local_cache(Model, transaction=None):
|
||||
@@ -132,26 +133,74 @@ class ModelStorage(Model):
|
||||
super(ModelStorage, cls).__setup__()
|
||||
if issubclass(cls, ModelView):
|
||||
cls.__rpc__.update({
|
||||
'create': RPC(readonly=False,
|
||||
'create': RPC(
|
||||
readonly=False,
|
||||
size_limits={
|
||||
0: _request_records_limit,
|
||||
},
|
||||
result=lambda r: list(map(int, r))),
|
||||
'read': RPC(timeout=_request_timeout),
|
||||
'write': RPC(readonly=False,
|
||||
instantiate=slice(0, None, 2)),
|
||||
'delete': RPC(readonly=False, instantiate=0),
|
||||
'copy': RPC(readonly=False, instantiate=0, unique=False,
|
||||
'read': RPC(
|
||||
timeout=_request_timeout,
|
||||
size_limits={
|
||||
0: _request_records_limit,
|
||||
}),
|
||||
'write': RPC(
|
||||
readonly=False,
|
||||
instantiate=slice(0, None, 2),
|
||||
size_limits={
|
||||
(0, None, 2), _request_records_limit,
|
||||
}),
|
||||
'delete': RPC(
|
||||
readonly=False,
|
||||
instantiate=0,
|
||||
size_limits={
|
||||
(0, None, 2): _request_records_limit,
|
||||
}),
|
||||
'copy': RPC(
|
||||
readonly=False,
|
||||
instantiate=0,
|
||||
unique=False,
|
||||
size_limits={
|
||||
0: _request_records_limit,
|
||||
},
|
||||
result=lambda r: list(map(int, r))),
|
||||
'search': RPC(
|
||||
result=lambda r: list(map(int, r)),
|
||||
size_limits={
|
||||
2: _request_records_limit,
|
||||
},
|
||||
timeout=_request_timeout),
|
||||
'search_count': RPC(timeout=_request_timeout),
|
||||
'search_read': RPC(timeout=_request_timeout),
|
||||
'search_count': RPC(
|
||||
timeout=_request_timeout,
|
||||
size_limits={
|
||||
2: _request_records_limit,
|
||||
}),
|
||||
'search_read': RPC(
|
||||
timeout=_request_timeout,
|
||||
size_limits={
|
||||
2: _request_records_limit,
|
||||
}),
|
||||
'resources': RPC(
|
||||
instantiate=0, unique=False,
|
||||
instantiate=0,
|
||||
unique=False,
|
||||
timeout=_request_timeout),
|
||||
'export_data_domain': RPC(timeout=_request_timeout),
|
||||
'export_data_domain': RPC(
|
||||
timeout=_request_timeout,
|
||||
size_limits={
|
||||
4: _request_records_limit,
|
||||
}),
|
||||
'export_data': RPC(
|
||||
instantiate=0, unique=False, timeout=_request_timeout),
|
||||
'import_data': RPC(readonly=False),
|
||||
instantiate=0,
|
||||
unique=False,
|
||||
size_limits={
|
||||
0: _request_records_limit,
|
||||
},
|
||||
timeout=_request_timeout),
|
||||
'import_data': RPC(
|
||||
readonly=False,
|
||||
size_limits={
|
||||
1: _request_records_limit,
|
||||
}),
|
||||
})
|
||||
if cls._log is None:
|
||||
cls._log = not cls.__name__.startswith('ir.')
|
||||
|
||||
@@ -6,6 +6,7 @@ from functools import wraps
|
||||
from lxml import etree
|
||||
|
||||
from trytond.cache import Cache
|
||||
from trytond.config import config
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.i18n import gettext
|
||||
from trytond.pool import Pool
|
||||
@@ -19,6 +20,7 @@ from .fields import on_change_result
|
||||
from .model import Model
|
||||
|
||||
__all__ = ['ModelView']
|
||||
_request_records_limit = config.getint('request', 'records_limit')
|
||||
|
||||
|
||||
class AccessButtonError(UserError):
|
||||
@@ -84,15 +86,24 @@ class ModelView(Model):
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(ModelView, cls).__setup__()
|
||||
cls.__rpc__['fields_view_get'] = RPC(cache=dict(days=1))
|
||||
cls.__rpc__['view_toolbar_get'] = RPC(cache=dict(days=1))
|
||||
cls.__rpc__['on_change'] = RPC(instantiate=0, result=on_change_result)
|
||||
cls.__rpc__['on_change_with'] = RPC(
|
||||
instantiate=0, result=on_change_result)
|
||||
cls.__rpc__['on_change_notify'] = RPC(instantiate=0)
|
||||
cls.__rpc__['on_scan_code'] = RPC(
|
||||
instantiate=0, result=on_change_result)
|
||||
cls.__rpc__['autocomplete'] = RPC()
|
||||
cls.__rpc__.update({
|
||||
'fields_view_get': RPC(
|
||||
cache=dict(days=1),
|
||||
size_limits={
|
||||
2: 1,
|
||||
}),
|
||||
'view_toolbar_get': RPC(cache=dict(days=1)),
|
||||
'on_change': RPC(instantiate=0, result=on_change_result),
|
||||
'on_change_with': RPC(
|
||||
instantiate=0, result=on_change_result),
|
||||
'on_change_notify': RPC(instantiate=0),
|
||||
'on_scan_code': RPC(
|
||||
instantiate=0, result=on_change_result),
|
||||
'autocomplete': RPC(
|
||||
size_limits={
|
||||
2: _request_records_limit,
|
||||
}),
|
||||
})
|
||||
cls._buttons = {}
|
||||
|
||||
fields_ = {}
|
||||
|
||||
+34
-2
@@ -2,8 +2,10 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import copy
|
||||
import datetime as dt
|
||||
from collections.abc import Sized
|
||||
|
||||
from trytond.exceptions import TrytonException
|
||||
from trytond.protocols.wrappers import HTTPStatus, abort
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
@@ -17,16 +19,19 @@ class RPC(object):
|
||||
check_access: If access right must be checked
|
||||
fresh_session: If a fresh session is required
|
||||
unique: Check instances are unique
|
||||
timeout: The timeout in second
|
||||
size_limits: A dictionary with size limits
|
||||
'''
|
||||
|
||||
__slots__ = (
|
||||
'readonly', 'instantiate', 'decorator', 'result',
|
||||
'check_access', 'fresh_session', 'unique', 'cache', 'timeout')
|
||||
'check_access', 'fresh_session', 'unique', 'cache', 'timeout',
|
||||
'size_limits')
|
||||
|
||||
def __init__(
|
||||
self, readonly=True, instantiate=None, decorator=None, result=None,
|
||||
check_access=True, fresh_session=False, unique=True, cache=None,
|
||||
timeout=None):
|
||||
timeout=None, size_limits=None):
|
||||
self.readonly = readonly
|
||||
self.instantiate = instantiate
|
||||
self.decorator = decorator
|
||||
@@ -42,6 +47,7 @@ class RPC(object):
|
||||
cache = RPCCache(**cache)
|
||||
self.cache = cache
|
||||
self.timeout = timeout
|
||||
self.size_limits = size_limits
|
||||
|
||||
def convert(self, obj, *args, **kwargs):
|
||||
args = list(args)
|
||||
@@ -87,8 +93,34 @@ class RPC(object):
|
||||
args[self.instantiate] = instance(data)
|
||||
if self.check_access:
|
||||
context['_check_access'] = True
|
||||
self._check_size_limits(args, kwargs)
|
||||
return args, kwargs, context, timestamp
|
||||
|
||||
def _check_size_limits(self, args, kwargs):
|
||||
if not self.size_limits:
|
||||
return
|
||||
for arg_spec, limit in self.size_limits.items():
|
||||
if not limit:
|
||||
continue
|
||||
if isinstance(arg_spec, int):
|
||||
try:
|
||||
selection = [args[arg_spec]]
|
||||
except IndexError:
|
||||
continue
|
||||
else:
|
||||
selection = args[slice(*arg_spec)]
|
||||
test = 0
|
||||
for arg in selection:
|
||||
if isinstance(arg, Sized):
|
||||
test += len(arg)
|
||||
else:
|
||||
try:
|
||||
test += arg
|
||||
except TypeError:
|
||||
pass
|
||||
if test > limit:
|
||||
abort(HTTPStatus.REQUEST_ENTITY_TOO_LARGE)
|
||||
|
||||
def decorate(self, func):
|
||||
if self.decorator:
|
||||
func = self.decorator(func)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from unittest.mock import DEFAULT, Mock, call
|
||||
|
||||
from trytond.protocols.wrappers import exceptions
|
||||
from trytond.rpc import RPC
|
||||
from trytond.tests.test_tryton import (
|
||||
TestCase, activate_module, with_transaction)
|
||||
@@ -142,3 +143,47 @@ class RPCTestCase(TestCase):
|
||||
self.assertEqual(
|
||||
rpc_with_access.convert(None, {}),
|
||||
([], {}, {'_check_access': True}, None))
|
||||
|
||||
@with_transaction()
|
||||
def test_size_limits_integer(self):
|
||||
"Test size_limits integer"
|
||||
obj = Mock()
|
||||
rpc = RPC(size_limits={0: 42})
|
||||
|
||||
rpc.convert(obj, list(range(10)), {})
|
||||
|
||||
with self.assertRaises(exceptions.RequestEntityTooLarge):
|
||||
rpc.convert(obj, list(range(43)), {})
|
||||
|
||||
@with_transaction()
|
||||
def test_size_limits_slice(self):
|
||||
"Test size_limits slice"
|
||||
obj = Mock()
|
||||
rpc = RPC(size_limits={(0, None, 2): 42})
|
||||
|
||||
rpc.convert(obj, list(range(10)), None, list(range(10)), {})
|
||||
|
||||
with self.assertRaises(exceptions.RequestEntityTooLarge):
|
||||
rpc.convert(obj, list(range(30)), None, list(range(30)), {})
|
||||
|
||||
@with_transaction()
|
||||
def test_size_limits_integer_number(self):
|
||||
"Test size_limits integer number"
|
||||
obj = Mock()
|
||||
rpc = RPC(size_limits={0: 42})
|
||||
|
||||
rpc.convert(obj, 10, {})
|
||||
|
||||
with self.assertRaises(exceptions.RequestEntityTooLarge):
|
||||
rpc.convert(obj, 43, {})
|
||||
|
||||
@with_transaction()
|
||||
def test_size_limits_slice_number(self):
|
||||
"Test size_limits slice"
|
||||
obj = Mock()
|
||||
rpc = RPC(size_limits={(0, None, 2): 42})
|
||||
|
||||
rpc.convert(obj, 10, None, 10, {})
|
||||
|
||||
with self.assertRaises(exceptions.RequestEntityTooLarge):
|
||||
rpc.convert(obj, 30, None, 30, {})
|
||||
|
||||
Reference in New Issue
Block a user