From 3037cab43e443a15406a9c128b785e32e58ba533 Mon Sep 17 00:00:00 2001 From: Ernad Husremovic Date: Mon, 9 Mar 2026 15:19:28 +0100 Subject: [PATCH] add rpc addon and fix 19.0 compatibility issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add missing rpc addon (auto_install, required by server_wide_modules) - add __init__.py -> init.py symlink for odoo package importability - re-export image_process from odoo.tools (needed by web_editor) - add backward-compatible slug/unslug functions in http_routing 🤖 assisted by claude --- odoo-bringout-oca-ocb-base/odoo/__init__.py | 1 + .../odoo/tools/__init__.py | 1 + .../http_routing/models/ir_http.py | 11 + odoo-bringout-oca-ocb-rpc/rpc/__init__.py | 1 + odoo-bringout-oca-ocb-rpc/rpc/__manifest__.py | 15 + .../rpc/controllers/__init__.py | 29 ++ .../rpc/controllers/json2.py | 90 ++++++ .../rpc/controllers/jsonrpc.py | 16 + .../rpc/controllers/xmlrpc.py | 169 ++++++++++ .../rpc/tests/__init__.py | 1 + .../rpc/tests/test_xmlrpc.py | 295 ++++++++++++++++++ 11 files changed, 629 insertions(+) create mode 120000 odoo-bringout-oca-ocb-base/odoo/__init__.py create mode 100644 odoo-bringout-oca-ocb-rpc/rpc/__init__.py create mode 100644 odoo-bringout-oca-ocb-rpc/rpc/__manifest__.py create mode 100644 odoo-bringout-oca-ocb-rpc/rpc/controllers/__init__.py create mode 100644 odoo-bringout-oca-ocb-rpc/rpc/controllers/json2.py create mode 100644 odoo-bringout-oca-ocb-rpc/rpc/controllers/jsonrpc.py create mode 100644 odoo-bringout-oca-ocb-rpc/rpc/controllers/xmlrpc.py create mode 100644 odoo-bringout-oca-ocb-rpc/rpc/tests/__init__.py create mode 100644 odoo-bringout-oca-ocb-rpc/rpc/tests/test_xmlrpc.py diff --git a/odoo-bringout-oca-ocb-base/odoo/__init__.py b/odoo-bringout-oca-ocb-base/odoo/__init__.py new file mode 120000 index 00000000..4a32c972 --- /dev/null +++ b/odoo-bringout-oca-ocb-base/odoo/__init__.py @@ -0,0 +1 @@ +init.py \ No newline at end of file diff --git a/odoo-bringout-oca-ocb-base/odoo/tools/__init__.py b/odoo-bringout-oca-ocb-base/odoo/tools/__init__.py index 77ece01f..f2683709 100644 --- a/odoo-bringout-oca-ocb-base/odoo/tools/__init__.py +++ b/odoo-bringout-oca-ocb-base/odoo/tools/__init__.py @@ -18,3 +18,4 @@ from .translate import _, html_translate, xml_translate, LazyTranslate from .xml_utils import cleanup_xml_node, load_xsd_files_from_url, validate_xml_from_attachment from .convert import convert_csv_import, convert_file, convert_sql_import, convert_xml_import from .set_expression import SetDefinitions +from .image import image_process, image_data_uri, base64_to_image, image_to_base64 diff --git a/odoo-bringout-oca-ocb-http_routing/http_routing/models/ir_http.py b/odoo-bringout-oca-ocb-http_routing/http_routing/models/ir_http.py index d508ceb8..c8bb97fb 100644 --- a/odoo-bringout-oca-ocb-http_routing/http_routing/models/ir_http.py +++ b/odoo-bringout-oca-ocb-http_routing/http_routing/models/ir_http.py @@ -628,3 +628,14 @@ class IrHttp(models.AbstractModel): except werkzeug.exceptions.NotFound: new_url = path return new_url or path, endpoint and endpoint[0] + + +# Backward-compatible module-level functions that delegate to IrHttp class methods. +# In Odoo 19.0 these were moved to IrHttp._slug/_unslug, but many addons +# still import them as standalone functions. +def slug(value): + return IrHttp._slug(value) + + +def unslug(value): + return IrHttp._unslug(value) diff --git a/odoo-bringout-oca-ocb-rpc/rpc/__init__.py b/odoo-bringout-oca-ocb-rpc/rpc/__init__.py new file mode 100644 index 00000000..e046e49f --- /dev/null +++ b/odoo-bringout-oca-ocb-rpc/rpc/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/odoo-bringout-oca-ocb-rpc/rpc/__manifest__.py b/odoo-bringout-oca-ocb-rpc/rpc/__manifest__.py new file mode 100644 index 00000000..f65d661e --- /dev/null +++ b/odoo-bringout-oca-ocb-rpc/rpc/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': 'RPC endpoints', + 'description': """\ +Standard Odoo RPC endpoints to models +===================================== + +This module provides the /xmlrpc and /jsonrpc endpoints used to +programmatically access models. +""", + 'depends': ["base"], + 'category': 'Extra Tools', + 'auto_install': True, + 'author': 'Odoo S.A.', + 'license': 'LGPL-3', +} diff --git a/odoo-bringout-oca-ocb-rpc/rpc/controllers/__init__.py b/odoo-bringout-oca-ocb-rpc/rpc/controllers/__init__.py new file mode 100644 index 00000000..5e82953e --- /dev/null +++ b/odoo-bringout-oca-ocb-rpc/rpc/controllers/__init__.py @@ -0,0 +1,29 @@ +import odoo.release +from odoo.http import request, route + +from . import json2 + +RPC_DEPRECATION_NOTICE = """\ +The /xmlrpc, /xmlrpc/2 and /jsonrpc endpoints are deprecated in Odoo 19 \ +and scheduled for removal in Odoo 20. Please report the problem to the \ +client making the request. +Mute this logger: --log-handler %s:ERROR +https://www.odoo.com/documentation/latest/developer/reference/external_api.html#migrating-from-xml-rpc-json-rpc""" + + +def _check_request(): + if request.db: + request.env.cr.close() + + +from .jsonrpc import JSONRPC # noqa: E402 +from .xmlrpc import XMLRPC # noqa: E402 + + +class RPC(XMLRPC, JSONRPC): + @route(['/web/version', '/json/version'], type='http', auth='none', readonly=True) + def version(self): + return request.make_json_response({ + 'version_info': odoo.release.version_info, + 'version': odoo.release.version, + }) diff --git a/odoo-bringout-oca-ocb-rpc/rpc/controllers/json2.py b/odoo-bringout-oca-ocb-rpc/rpc/controllers/json2.py new file mode 100644 index 00000000..8a8abf47 --- /dev/null +++ b/odoo-bringout-oca-ocb-rpc/rpc/controllers/json2.py @@ -0,0 +1,90 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import inspect +import logging +from collections.abc import Mapping, Sequence +from typing import Any + +from werkzeug.exceptions import ( + NotFound, + UnprocessableEntity, +) + +from odoo import http +from odoo.http import request +from odoo.models import BaseModel +from odoo.service.model import get_public_method +from odoo.tools import frozendict + +_logger = logging.getLogger(__name__) + + +class WebJson2Controller(http.Controller): + def _web_json_2_rpc_readonly(self, rule, args): + try: + model_name = args['__model__'] + method_name = args['__method__'] + Model = request.registry[model_name] + except KeyError: + # no need of a read/write cursor to send a 404 http error + return True + for cls in Model.mro(): + method = getattr(cls, method_name, None) + if method is not None and hasattr(method, '_readonly'): + return method._readonly + return False + + # Take over /json/ + @http.route( + ['/json/2', '/json/2/'], + auth='public', + type='json2', + readonly=True, + methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + ) + def web_json_2_404(self, subpath=None): + e = "Did you mean POST /json/2//?" + raise request.not_found(e) + + @http.route( + '/json/2/<__model__>/<__method__>', + methods=['POST'], + auth='bearer', + type='json2', + readonly=_web_json_2_rpc_readonly, + save_session=False, + ) + def web_json_2_rpc( + self, + __model__: str, + __method__: str, + ids: Sequence[int] = (), + context: Mapping[str, Any] = frozendict(), + **kwargs, + ): + try: + Model = request.env[__model__].with_context(context) + except KeyError as exc: + e = f"the model {__model__!r} does not exist" + raise NotFound(e) from exc + + try: + func = get_public_method(Model, __method__) + except AttributeError as exc: + raise NotFound(exc.args[0]) from exc + if hasattr(func, '_api_model') and ids: + e = f"cannot call {__model__}.{__method__} with ids" + raise UnprocessableEntity(e) + + records = Model.browse(ids) + signature = inspect.signature(func) + try: + signature.bind(records, **kwargs) + except TypeError as exc: + raise UnprocessableEntity(exc.args[0]) + + result = func(records, **kwargs) + if isinstance(result, BaseModel): + result = result.ids + + return result diff --git a/odoo-bringout-oca-ocb-rpc/rpc/controllers/jsonrpc.py b/odoo-bringout-oca-ocb-rpc/rpc/controllers/jsonrpc.py new file mode 100644 index 00000000..851fe061 --- /dev/null +++ b/odoo-bringout-oca-ocb-rpc/rpc/controllers/jsonrpc.py @@ -0,0 +1,16 @@ +import logging + +from odoo.http import Controller, dispatch_rpc, route + +from . import RPC_DEPRECATION_NOTICE, _check_request + +logger = logging.getLogger(__name__) + + +class JSONRPC(Controller): + @route('/jsonrpc', type='jsonrpc', auth="none", save_session=False) + def jsonrpc(self, service, method, args): + """ Method used by client APIs to contact OpenERP. """ + logger.warning(RPC_DEPRECATION_NOTICE, __name__) + _check_request() + return dispatch_rpc(service, method, args) diff --git a/odoo-bringout-oca-ocb-rpc/rpc/controllers/xmlrpc.py b/odoo-bringout-oca-ocb-rpc/rpc/controllers/xmlrpc.py new file mode 100644 index 00000000..fbdb658c --- /dev/null +++ b/odoo-bringout-oca-ocb-rpc/rpc/controllers/xmlrpc.py @@ -0,0 +1,169 @@ +import logging +import sys +import traceback +import xmlrpc.client +from collections import defaultdict +from datetime import date, datetime + +from markupsafe import Markup + +import odoo.exceptions +from odoo.fields import Command, Date, Datetime +from odoo.http import Controller, Response, dispatch_rpc, request, route +from odoo.tools import lazy +from odoo.tools.misc import frozendict + +from . import RPC_DEPRECATION_NOTICE, _check_request + +logger = logging.getLogger(__name__) + +# XML-RPC fault codes. Some care must be taken when changing these: the +# constants are also defined client-side and must remain in sync. +# User code must use the exceptions defined in ``odoo.exceptions`` (not +# create directly ``xmlrpc.client.Fault`` objects). +RPC_FAULT_CODE_CLIENT_ERROR = 1 # indistinguishable from app. error. +RPC_FAULT_CODE_APPLICATION_ERROR = 1 +RPC_FAULT_CODE_WARNING = 2 +RPC_FAULT_CODE_ACCESS_DENIED = 3 +RPC_FAULT_CODE_ACCESS_ERROR = 4 + +# 0 to 31, excluding tab, newline, and carriage return +CONTROL_CHARACTERS = dict.fromkeys(set(range(32)) - {9, 10, 13}) + + +def xmlrpc_handle_exception_int(e): + if isinstance(e, odoo.exceptions.RedirectWarning): + fault = xmlrpc.client.Fault(RPC_FAULT_CODE_WARNING, str(e)) + elif isinstance(e, odoo.exceptions.AccessError): + fault = xmlrpc.client.Fault(RPC_FAULT_CODE_ACCESS_ERROR, str(e)) + elif isinstance(e, odoo.exceptions.AccessDenied): + fault = xmlrpc.client.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e)) + elif isinstance(e, odoo.exceptions.UserError): + fault = xmlrpc.client.Fault(RPC_FAULT_CODE_WARNING, str(e)) + else: + info = sys.exc_info() + formatted_info = "".join(traceback.format_exception(*info)) + fault = xmlrpc.client.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info) + + return dumps(fault) + + +def xmlrpc_handle_exception_string(e): + if isinstance(e, odoo.exceptions.RedirectWarning): + fault = xmlrpc.client.Fault(f'warning -- Warning\n\n{e}', '') + elif isinstance(e, odoo.exceptions.MissingError): + fault = xmlrpc.client.Fault(f'warning -- MissingError\n\n{e}', '') + elif isinstance(e, odoo.exceptions.AccessError): + fault = xmlrpc.client.Fault(f'warning -- AccessError\n\n{e}', '') + elif isinstance(e, odoo.exceptions.AccessDenied): + fault = xmlrpc.client.Fault('AccessDenied', str(e)) + elif isinstance(e, odoo.exceptions.UserError): + fault = xmlrpc.client.Fault(f'warning -- UserError\n\n{e}', '') + # InternalError + else: + info = sys.exc_info() + formatted_info = "".join(traceback.format_exception(*info)) + fault = xmlrpc.client.Fault(odoo.tools.exception_to_unicode(e), formatted_info) + + return dumps(fault) + + +class OdooMarshaller(xmlrpc.client.Marshaller): + dispatch = dict(xmlrpc.client.Marshaller.dispatch) + + def dump_frozen_dict(self, value, write): + value = dict(value) + self.dump_struct(value, write) + + # By default, in xmlrpc, bytes are converted to xmlrpc.client.Binary object. + # Historically, odoo is sending binary as base64 string. + # In python 3, base64.b64{de,en}code() methods now works on bytes. + def dump_bytes(self, value, write): + self.dump_unicode(value.decode(), write) + + def dump_datetime(self, value, write): + # override to marshall as a string for backwards compatibility + value = Datetime.to_string(value) + self.dump_unicode(value, write) + + # convert date objects to strings in iso8061 format. + def dump_date(self, value, write): + value = Date.to_string(value) + self.dump_unicode(value, write) + + def dump_lazy(self, value, write): + v = value._value + return self.dispatch[type(v)](self, v, write) + + def dump_unicode(self, value, write): + # XML 1.0 disallows control characters, remove them otherwise they break clients + return super().dump_unicode(value.translate(CONTROL_CHARACTERS), write) + + dispatch[frozendict] = dump_frozen_dict + dispatch[bytes] = dump_bytes + dispatch[datetime] = dump_datetime + dispatch[date] = dump_date + dispatch[lazy] = dump_lazy + dispatch[str] = dump_unicode + dispatch[Command] = dispatch[int] + dispatch[defaultdict] = dispatch[dict] + dispatch[Markup] = lambda self, value, write: self.dispatch[str](self, str(value), write) + + +def dumps(params: list | tuple | xmlrpc.client.Fault) -> str: + response = OdooMarshaller(allow_none=False).dumps(params) + return f"""\ + + +{response} + +""" + +# ========================================================== +# RPC Controller +# ========================================================== + + +class XMLRPC(Controller): + """Handle RPC connections.""" + + def _xmlrpc(self, service): + """Common method to handle an XML-RPC request.""" + data = request.httprequest.get_data() + params, method = xmlrpc.client.loads(data, use_datetime=True) + result = dispatch_rpc(service, method, params) + return dumps((result,)) + + @route("/xmlrpc/", auth="none", methods=["POST"], csrf=False, save_session=False) + def xmlrpc_1(self, service): + """XML-RPC service that returns faultCode as strings. + + This entrypoint is historical and non-compliant, but kept for + backwards-compatibility. + """ + logger.warning(RPC_DEPRECATION_NOTICE, __name__) + _check_request() + try: + response = self._xmlrpc(service) + except Exception as error: + error.error_response = Response( + response=xmlrpc_handle_exception_string(error), + mimetype='text/xml', + ) + raise + return Response(response=response, mimetype='text/xml') + + @route("/xmlrpc/2/", auth="none", methods=["POST"], csrf=False, save_session=False) + def xmlrpc_2(self, service): + """XML-RPC service that returns faultCode as int.""" + logger.warning(RPC_DEPRECATION_NOTICE, __name__) + _check_request() + try: + response = self._xmlrpc(service) + except Exception as error: + error.error_response = Response( + response=xmlrpc_handle_exception_int(error), + mimetype='text/xml', + ) + raise + return Response(response=response, mimetype='text/xml') diff --git a/odoo-bringout-oca-ocb-rpc/rpc/tests/__init__.py b/odoo-bringout-oca-ocb-rpc/rpc/tests/__init__.py new file mode 100644 index 00000000..0c9f8d52 --- /dev/null +++ b/odoo-bringout-oca-ocb-rpc/rpc/tests/__init__.py @@ -0,0 +1 @@ +from . import test_xmlrpc diff --git a/odoo-bringout-oca-ocb-rpc/rpc/tests/test_xmlrpc.py b/odoo-bringout-oca-ocb-rpc/rpc/tests/test_xmlrpc.py new file mode 100644 index 00000000..41bc29d6 --- /dev/null +++ b/odoo-bringout-oca-ocb-rpc/rpc/tests/test_xmlrpc.py @@ -0,0 +1,295 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import collections +import datetime +import time + +import odoo +import odoo.tools +from odoo.exceptions import AccessDenied, AccessError +from odoo.http import _request_stack +from odoo.service import common as auth +from odoo.service import model +from odoo.tests import common +from odoo.tools import DotDict, mute_logger + +from odoo.addons.base.tests.common import SavepointCaseWithUserDemo + + +class TestExternalAPI(SavepointCaseWithUserDemo): + + def test_call_kw(self): + """kwargs is not modified by the execution of the call""" + partner = self.env['res.partner'].create({'name': 'MyPartner1'}) + args = (partner.ids, ['name']) + kwargs = {'context': {'test': True}} + model.call_kw(self.env['res.partner'], 'read', args, kwargs) + self.assertEqual(kwargs, {'context': {'test': True}}) + + +@common.tagged('post_install', '-at_install') +class TestXMLRPC(common.HttpCase): + + def setUp(self): + super(TestXMLRPC, self).setUp() + self.admin_uid = self.env.ref('base.user_admin').id + + ml_xml = mute_logger('odoo.addons.rpc.controllers.xmlrpc') + ml_xml.__enter__() # noqa: PLC2801 + self.addCleanup(ml_xml.__exit__) + + ml_json = mute_logger('odoo.addons.rpc.controllers.jsonrpc') + ml_json.__enter__() # noqa: PLC2801 + self.addCleanup(ml_json.__exit__) + + def xmlrpc(self, model, method, *args, **kwargs): + return self.xmlrpc_object.execute_kw( + common.get_db_name(), self.admin_uid, 'admin', + model, method, args, kwargs + ) + + def test_01_xmlrpc_login(self): + """ Try to login on the common service. """ + db_name = common.get_db_name() + uid = self.xmlrpc_common.login(db_name, 'admin', 'admin') + self.assertEqual(uid, self.admin_uid) + + def test_xmlrpc_ir_model_search(self): + """ Try a search on the object service. """ + o = self.xmlrpc_object + db_name = common.get_db_name() + ids = o.execute(db_name, self.admin_uid, 'admin', 'ir.model', 'search', []) + self.assertIsInstance(ids, list) + ids = o.execute(db_name, self.admin_uid, 'admin', 'ir.model', 'search', [], {}) + self.assertIsInstance(ids, list) + + def test_xmlrpc_datetime(self): + """ Test that native datetime can be sent over xmlrpc + """ + m = self.env.ref('base.model_res_device_log') + self.env['ir.model.access'].create({ + 'name': "w/e", + 'model_id': m.id, + 'perm_read': True, + 'perm_create': True, + }) + + now = datetime.datetime.now() + ids = self.xmlrpc( + 'res.device.log', 'create', + {'session_identifier': "abc", 'first_activity': now, 'revoked': False} + ) + [r] = self.xmlrpc( + 'res.device.log', 'read', + ids, ['first_activity'], + ) + self.assertEqual(r['first_activity'], now.isoformat(" ", "seconds")) + + def test_xmlrpc_read_group(self): + self.xmlrpc_object.execute( + common.get_db_name(), self.admin_uid, 'admin', + 'res.partner', 'formatted_read_group', [], ['parent_id'], ['color:sum'], + ) + + def test_xmlrpc_name_search(self): + self.xmlrpc_object.execute( + common.get_db_name(), self.admin_uid, 'admin', + 'res.partner', 'name_search', "admin" + ) + + def test_xmlrpc_html_field(self): + sig = '

bork bork bork bork

' + r = self.env['res.users'].create({ + 'name': 'bob', + 'login': 'bob', + 'signature': sig + }) + self.assertEqual(str(r.signature), sig) + [x] = self.xmlrpc('res.users', 'read', r.id, ['signature']) + self.assertEqual(x['signature'], sig) + + def test_xmlrpc_frozendict_marshalling(self): + """ Test that the marshalling of a frozendict object works properly over XMLRPC """ + self.env.ref('base.user_admin').tz = "Europe/Brussels" + ctx = self.xmlrpc_object.execute( + common.get_db_name(), self.admin_uid, 'admin', + 'res.users', 'context_get', + ) + self.assertEqual(ctx['lang'], 'en_US') + self.assertEqual(ctx['tz'], 'Europe/Brussels') + + def test_xmlrpc_defaultdict_marshalling(self): + """ + Test that the marshalling of a collections.defaultdict object + works properly over XMLRPC + """ + self.patch(self.registry['res.users'], 'context_get', + odoo.api.model(lambda *_: collections.defaultdict(int))) + self.assertEqual(self.xmlrpc('res.users', 'context_get'), {}) + + def test_xmlrpc_remove_control_characters(self): + record = self.env['res.users'].create({ + 'name': 'bob with a control character: \x03', + 'login': 'bob', + }) + self.assertEqual(record.name, 'bob with a control character: \x03') + [record_data] = self.xmlrpc('res.users', 'read', record.id, ['name']) + self.assertEqual(record_data['name'], 'bob with a control character: ') + + def test_jsonrpc_read_group(self): + self._json_call( + common.get_db_name(), self.admin_uid, 'admin', + 'res.partner', 'formatted_read_group', [], ['parent_id'], ['color:sum'], + ) + + def test_jsonrpc_name_search(self): + # well that's some sexy sexy call right there + self._json_call( + common.get_db_name(), + self.admin_uid, 'admin', + 'res.partner', 'name_search', 'admin' + ) + + def _json_call(self, *args): + self.url_open(f"{self.base_url()}/jsonrpc", json={ + 'jsonrpc': '2.0', + 'id': None, + 'method': 'call', + 'params': { + 'service': 'object', + 'method': 'execute', + 'args': args + } + }) + + def test_xmlrpc_attachment_raw(self): + ids = self.env['ir.attachment'].create({'name': 'n', 'raw': b'\x01\x09'}).ids + [att] = self.xmlrpc_object.execute( + common.get_db_name(), self.admin_uid, 'admin', + 'ir.attachment', 'read', ids, ['raw']) + self.assertEqual(att['raw'], '\t', + "on read, binary data should be decoded as a string and stripped from control character") + +# really just for the test cursor +@common.tagged('post_install', '-at_install') +class TestAPIKeys(common.HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._user = cls.env['res.users'].create({ + 'name': "Bylan", + 'login': 'byl', + 'password': 'ananananan', + 'tz': 'Australia/Eucla', + }) + + def setUp(self): + super().setUp() + + def get_json_data(): + raise ValueError("There is no json here") + # needs a fake request in order to call methods protected with check_identity + self.http_request_key = self.canonical_tag + fake_req = DotDict({ + # various things go and access request items + 'httprequest': DotDict({ + 'environ': {'REMOTE_ADDR': 'localhost'}, + 'cookies': {common.TEST_CURSOR_COOKIE_NAME: self.canonical_tag}, + 'args': {}, + }), + 'cookies': {common.TEST_CURSOR_COOKIE_NAME: self.canonical_tag}, + # bypass check_identity flow + 'session': {'identity-check-last': time.time()}, + 'geoip': {}, + 'get_json_data': get_json_data, + }) + _request_stack.push(fake_req) + self.addCleanup(_request_stack.pop) + + def test_trivial(self): + uid = auth.dispatch('authenticate', [self.env.cr.dbname, 'byl', 'ananananan', {}]) + self.assertEqual(uid, self._user.id) + + ctx = model.dispatch('execute_kw', [ + self.env.cr.dbname, uid, 'ananananan', + 'res.users', 'context_get', [] + ]) + self.assertEqual(ctx['tz'], 'Australia/Eucla') + + def test_wrongpw(self): + # User.authenticate raises but RPC.authenticate returns False + uid = auth.dispatch('authenticate', [self.env.cr.dbname, 'byl', 'aws', {}]) + self.assertFalse(uid) + with self.assertRaises(AccessDenied): + model.dispatch('execute_kw', [ + self.env.cr.dbname, self._user.id, 'aws', + 'res.users', 'context_get', [] + ]) + + def test_key(self): + env = self.env(user=self._user) + r = env['res.users.apikeys.description'].create({ + 'name': 'a', + }).make_key() + k = r['context']['default_key'] + + uid = auth.dispatch('authenticate', [self.env.cr.dbname, 'byl', 'ananananan', {}]) + self.assertEqual(uid, self._user.id) + + uid = auth.dispatch('authenticate', [self.env.cr.dbname, 'byl', k, {}]) + self.assertEqual(uid, self._user.id) + + ctx = model.dispatch('execute_kw', [ + self.env.cr.dbname, uid, k, + 'res.users', 'context_get', [] + ]) + self.assertEqual(ctx['tz'], 'Australia/Eucla') + + api_key = model.call_kw( + model=self.env['res.users.apikeys.description'], + name='create', + args=[{'name': 'Name of the key'}], + kwargs={} + ) + self.assertTrue(isinstance(api_key, int)) + + def test_delete(self): + env = self.env(user=self._user) + env['res.users.apikeys.description'].create({'name': 'b',}).make_key() + env['res.users.apikeys.description'].create({'name': 'b',}).make_key() + env['res.users.apikeys.description'].create({'name': 'b',}).make_key() + k0, k1, k2 = env['res.users.apikeys'].search([]) + + # user can remove their own keys + k0.remove() + self.assertFalse(k0.exists()) + + # admin can remove user keys + k1.with_user(self.env.ref('base.user_admin')).remove () + self.assertFalse(k1.exists()) + + # other user can't remove user keys + u = self.env['res.users'].create({ + 'name': 'a', + 'login': 'a', + 'group_ids': self.env.ref('base.group_user').ids, + }) + with self.assertRaises(AccessError): + k2.with_user(u).remove() + + def test_disabled(self): + env = self.env(user=self._user) + k = env['res.users.apikeys.description'].create({'name': 'b',}).make_key()['context']['default_key'] + + self._user.active = False + + with self.assertRaises(AccessDenied): + model.dispatch('execute_kw', [ + self.env.cr.dbname, self._user.id, 'ananananan', + 'res.users', 'context_get', [] + ]) + + with self.assertRaises(AccessDenied): + model.dispatch('execute_kw', [ + self.env.cr.dbname, self._user.id, k, + 'res.users', 'context_get', [] + ])