-
-
Notifications
You must be signed in to change notification settings - Fork 118
[19.0][FIX] endpoint: validate registry sync #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 19.0
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). | ||
|
|
||
| import json | ||
| import symtable | ||
| import textwrap | ||
|
|
||
| import jsonschema | ||
|
|
@@ -43,6 +44,10 @@ | |
| ["new", "compare_digest"], | ||
| ) | ||
|
|
||
| # ``safe_eval`` injects these built-ins at runtime. Keep validation aligned with | ||
| # Odoo instead of maintaining a second, potentially outdated list here. | ||
| _SAFE_EVAL_BUILTINS = frozenset(safe_eval._BUILTINS) | ||
|
|
||
|
|
||
| @disable_rpc() # Block ALL RPC calls | ||
| class EndpointMixin(models.AbstractModel): | ||
|
|
@@ -98,6 +103,62 @@ def _validate_exec__code(self): | |
| ) | ||
| ) | ||
|
|
||
| def _registry_sync_errors(self): | ||
| errors = super()._registry_sync_errors() | ||
| snippet = self.code_snippet or "" | ||
| syntax_error = safe_eval.test_python_expr(snippet, mode="exec") | ||
| if syntax_error: | ||
| errors.append( | ||
| self.env._("Invalid code snippet: %(error)s", error=syntax_error) | ||
| ) | ||
| return errors | ||
|
|
||
| unavailable_names = self._code_snippet_unavailable_names(snippet) | ||
| if unavailable_names: | ||
| errors.append( | ||
| self.env._( | ||
| "The code snippet uses unavailable variable(s): %(names)s. " | ||
| "Available system variables are: %(available_names)s.", | ||
| names=", ".join(unavailable_names), | ||
| available_names=", ".join( | ||
| sorted(self._code_snippet_system_variable_names()) | ||
| ), | ||
| ) | ||
| ) | ||
| return errors | ||
|
|
||
| def _code_snippet_system_variable_names(self): | ||
| # Derive names from the actual evaluation context so this validation | ||
| # stays accurate when another system variable is added. | ||
| return set(self._get_code_snippet_eval_context(request=None)) | ||
|
|
||
| def _code_snippet_unavailable_names(self, snippet): | ||
| """Find global names that safe_eval will not provide at runtime.""" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure to understand why we need this |
||
| symbol_table = symtable.symtable(snippet, "<endpoint>", "exec") | ||
| referenced_globals = set() | ||
|
|
||
| # ``symtable`` distinguishes global lookups from local names in nested | ||
| # functions and comprehensions, avoiding warnings for valid variables. | ||
| def collect_globals(table): | ||
| for symbol in table.get_symbols(): | ||
| if symbol.is_referenced() and symbol.is_global(): | ||
| referenced_globals.add(symbol.get_name()) | ||
| for child in table.get_children(): | ||
| collect_globals(child) | ||
|
|
||
| collect_globals(symbol_table) | ||
| assigned_names = { | ||
| symbol.get_name() | ||
| for symbol in symbol_table.get_symbols() | ||
| if symbol.is_assigned() or symbol.is_imported() | ||
| } | ||
| available_names = ( | ||
| self._code_snippet_system_variable_names() | ||
| | _SAFE_EVAL_BUILTINS | ||
| | assigned_names | ||
| ) | ||
| return sorted(referenced_globals - available_names) | ||
|
|
||
| def _get_request_content_schema_applicable_for_types(self): | ||
| """Content types for which ``request_content_schema`` applies.""" | ||
| return ["application/json", "application/xml"] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
|
|
||
| import logging | ||
|
|
||
| import werkzeug | ||
|
|
||
| from odoo import api, exceptions, fields, models | ||
|
|
||
| ENDPOINT_ROUTE_CONSUMER_MODELS = { | ||
|
|
@@ -187,6 +189,28 @@ def _check_route(self): | |
| ) | ||
| ) | ||
|
|
||
| def _registry_sync_errors(self): | ||
| errors = super()._registry_sync_errors() | ||
| try: | ||
| # Binding the rule performs the same converter lookup as Odoo's | ||
| # routing map without modifying the live endpoint registry. | ||
| routing_map = werkzeug.routing.Map( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To my knowledge dynamic rules are not supported by default, at least in the context of |
||
| strict_slashes=False, | ||
| converters=self.env["ir.http"]._get_converters(), | ||
| ) | ||
| rule = werkzeug.routing.Rule(self.route) | ||
| rule.merge_slashes = False | ||
| routing_map.add(rule) | ||
| except (LookupError, TypeError, ValueError) as error: | ||
| errors.append( | ||
| self.env._( | ||
| "Invalid route %(route)s: %(error)s", | ||
| route=self.route, | ||
| error=error, | ||
| ) | ||
| ) | ||
| return errors | ||
|
|
||
| @api.constrains("request_method", "request_content_type") | ||
| def _check_request_method(self): | ||
| for rec in self: | ||
|
|
@@ -207,6 +231,8 @@ def _endpoint_registry_unique_key(self): | |
| # TODO: consider if useful or not for single records | ||
| def _register_single_controller(self, options=None, key=None, init=False): | ||
| """Shortcut to register one single controller.""" | ||
| # Programmatic callers can bypass registry_sync and must be protected too. | ||
| self._validate_registry_sync(active_only=False) | ||
| rule = self._make_controller_rule(options=options, key=key) | ||
| self._endpoint_registry.update_rules([rule], init=init) | ||
| self.env.registry.clear_cache("routing") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not bind this to the sync. This check should happen right after save.
What I would do:
code_snippet_is_valid, default=True, computedcode_snippet_errors, computed, renders the errors on the form in an alert box of type errorThe alternative is to use a constraint, which is probably better: I don't see any reason to save an endpoint that is invalid. And that's the same done by ir_actions* with
_check_python_code.