From 16beaa75cebf403940309d55147aa1fa2ed33195 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Tue, 21 Jul 2026 00:26:43 +0000 Subject: [PATCH] [FIX] ai_tool: resolve tool model metadata with sudo Building and dispatching a tool reads ir.model via model_id, which a plain internal user (base.group_user) cannot access. Resolve the model name with sudo() while keeping the actual tool execution under the caller's own permissions, so non-admin MCP keys can list and call tools. Add a regression test exercising a non-admin user through _get_tool_definition and _execute_tool. --- ai_tool/models/ai_tool.py | 19 ++++++++++++------- ai_tool/tests/test_ai_tool.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/ai_tool/models/ai_tool.py b/ai_tool/models/ai_tool.py index 82068020..2cfb52c6 100644 --- a/ai_tool/models/ai_tool.py +++ b/ai_tool/models/ai_tool.py @@ -37,7 +37,10 @@ class AiTool(models.Model): ) def _get_tool_definition(self): - func = getattr(self.env[self.model_id.model], self.function_name) + # model_id points to ir.model, which non-admin users cannot read. + # The model name is only metadata, so resolve it as sudo. + model = self.sudo().model_id.model + func = getattr(self.env[model], self.function_name) return { "name": self.name, "description": self.description, @@ -77,18 +80,20 @@ def _ai_post_message_parse_body(self, message): return plaintext2html(message) def _execute_tool(self, *args, record=None, **kwargs): + # model_id points to ir.model, which non-admin users cannot read. + # Resolve the model name as sudo (metadata only); the call below + # still runs with the caller's own rights via self.env[model]. + model = self.sudo().model_id.model if self.kind == "generic": - return getattr(self.env[self.model_id.model], self.function_name)( - *args, **kwargs - ) + return getattr(self.env[model], self.function_name)(*args, **kwargs) if not record: raise ValueError("Record must be provided for non-generic tools") if self.kind == "generic_model": - return getattr(self.env[self.model_id.model], self.function_name)( + return getattr(self.env[model], self.function_name)( *args, record=record, **kwargs ) - elif record._name != self.model_id.model: + elif record._name != model: raise ValueError( - f"Record model {record._name} does not match tool model {self.model_id.model}" + f"Record model {record._name} does not match tool model {model}" ) return getattr(record, self.function_name)(*args, **kwargs) or {} diff --git a/ai_tool/tests/test_ai_tool.py b/ai_tool/tests/test_ai_tool.py index c9090aaf..86c1d991 100644 --- a/ai_tool/tests/test_ai_tool.py +++ b/ai_tool/tests/test_ai_tool.py @@ -47,3 +47,21 @@ def test_post_message_tool_record_different_model(self): tool.kind = "record" with self.assertRaises(ValueError): tool._execute_tool(message="Hello World", record=self.partner) + + def test_tool_non_admin_user(self): + """A plain internal user (no ir.model access) must be able to build a + tool definition and execute a generic tool: resolving the tool's model + is metadata and must not require Administration/Access Rights.""" + user = self.env["res.users"].create( + { + "name": "Ai Tool Plain User", + "login": "ai_tool_plain_user", + "groups_id": [(6, 0, [self.env.ref("base.group_user").id])], + } + ) + self.assertFalse(user.has_group("base.group_system")) + tool = self.env.ref("ai_tool.current_date").with_user(user) + definition = tool._get_tool_definition() + self.assertEqual(definition["name"], "get_date") + result = tool._execute_tool() + self.assertIn("date", result)