From 06af30c7176c9c7785d728f2d93b2f4e3a935776 Mon Sep 17 00:00:00 2001 From: Ivan Yelizariev <iel@odoo.com> Date: Thu, 21 Apr 2022 14:56:58 +0000 Subject: [PATCH] [FIX] core: fix infinite loops with child_of/parent_of 1. Infinite loop may happen on using `parent_of`\`child_of` when there is a recursion in the tree (e.g. a record is marked as a parent of itself). Fix it by excluding seen records from the next iteration. 2. Another problem with `child_of` is `parent_id` that references to another model. For example, the `parent_id` may come from inherited model. It's the case with `res.users` and `res.partner` models. It may lead to a random search results. Avoid that by raising exception in case of wrong usage of the `child_of` operator. STEPS: In demo data, there is a partner called "Wood Corner" that is `res.partner(9,)` that has 3 sub-contacts. If we give Portal access to two of them, we end up with a database, where we have a `res.users(9,)` record that has a partner, which has a `parent_id` to "Wood corner". So this way, the user id is the same as the user's partner's parent contact id. After that open a shell and type: ``` env['res.partner'].search([["user_ids", "child_of", 9]]) ``` BEFORE: infinite loop (without change n.1) or random search results (when change n.1 is applied) AFTER: ValueError exception --- opw-2729740 closes odoo/odoo#122080 X-original-commit: 3c9b345708632a841e91386a059c35ee53044b98 Signed-off-by: Raphael Collet <rco@odoo.com> Signed-off-by: Ivan Elizaryev (iel) <iel@odoo.com> --- .../test_new_api/models/test_new_api.py | 18 +++++++++++++ .../test_new_api/security/ir.model.access.csv | 2 ++ .../test_new_api/tests/test_one2many.py | 25 +++++++++++++++++++ odoo/osv/expression.py | 6 +++-- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py index e45132208f7f..a58b7ce2cbf4 100644 --- a/odoo/addons/test_new_api/models/test_new_api.py +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -1437,3 +1437,21 @@ class Prisoner(models.Model): name = fields.Char('Name') ship_ids = fields.Many2many('test_new_api.ship', 'test_new_api_crew', 'prisoner_id', 'ship_id') + + +class Team(models.Model): + _name = 'test_new_api.team' + _description = 'Odoo Team' + + name = fields.Char() + parent_id = fields.Many2one('test_new_api.team') + member_ids = fields.One2many('test_new_api.team.member', 'team_id') + + +class TeamMember(models.Model): + _name = 'test_new_api.team.member' + _description = 'Odoo Developer' + + name = fields.Char('Name') + team_id = fields.Many2one('test_new_api.team') + parent_id = fields.Many2one('test_new_api.team', related='team_id.parent_id') diff --git a/odoo/addons/test_new_api/security/ir.model.access.csv b/odoo/addons/test_new_api/security/ir.model.access.csv index 8ce59cd9f9a3..e4f8b026a15d 100644 --- a/odoo/addons/test_new_api/security/ir.model.access.csv +++ b/odoo/addons/test_new_api/security/ir.model.access.csv @@ -83,3 +83,5 @@ access_test_new_api_crew,access_test_new_api_crew,model_test_new_api_crew,base.g access_test_new_api_ship,access_test_new_api_ship,model_test_new_api_ship,base.group_user,1,1,1,1 access_test_new_api_pirate,access_test_new_api_pirate,model_test_new_api_pirate,base.group_user,1,1,1,1 access_test_new_api_prisoner,access_test_new_api_prisoner,model_test_new_api_prisoner,base.group_user,1,1,1,1 +access_test_new_api_team,access_test_new_api_team,model_test_new_api_team,base.group_user,1,0,0,0 +access_test_new_api_team_member,access_test_new_api_team_member,model_test_new_api_team_member,base.group_user,1,0,0,0 diff --git a/odoo/addons/test_new_api/tests/test_one2many.py b/odoo/addons/test_new_api/tests/test_one2many.py index e73e7b5aba31..8c1b351483bf 100644 --- a/odoo/addons/test_new_api/tests/test_one2many.py +++ b/odoo/addons/test_new_api/tests/test_one2many.py @@ -302,3 +302,28 @@ class One2manyCase(TransactionCase): order.write({ 'line_ids': [Command.link(line0.id), Command.link(line1.id)], }) + + def test_parent_id(self): + Team = self.env['test_new_api.team'] + Member = self.env['test_new_api.team.member'] + + team1 = Team.create({'name': 'ORM'}) + team2 = Team.create({'name': 'Bugfix', 'parent_id': team1.id}) + team3 = Team.create({'name': 'Support', 'parent_id': team2.id}) + + Member.create({'name': 'Raphael', 'team_id': team1.id}) + member2 = Member.create({'name': 'Noura', 'team_id': team3.id}) + Member.create({'name': 'Ivan', 'team_id': team2.id}) + + # In this specific case... + self.assertEqual(member2.id, member2.team_id.parent_id.id) + + # ...we had an infinite recursion on making the following search. + with self.assertRaises(ValueError): + Team.search([('member_ids', 'child_of', member2.id)]) + + # Also, test a simple infinite loop if record is marked as a parent of itself + team1.parent_id = team1.id + # Check that the search is not stuck in the loop + Team.search([('id', 'parent_of', team1.id)]) + Team.search([('id', 'child_of', team1.id)]) diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index d3786a7b9045..e923e02d0975 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -539,11 +539,13 @@ class expression(object): # filtering of forbidden records is done by the rest of the # domain parent_name = parent or left_model._parent_name + if (left_model._name != left_model._fields[parent_name].comodel_name): + raise ValueError(f"Invalid parent field: {left_model._fields[parent_name]}") child_ids = set() records = left_model.sudo().browse(ids) while records: child_ids.update(records._ids) - records = records.search([(parent_name, 'in', records.ids)], order='id') + records = records.search([(parent_name, 'in', records.ids)], order='id') - records.browse(child_ids) domain = [('id', 'in', list(child_ids))] if prefix: return [(left, 'in', left_model._search(domain, order='id'))] @@ -571,7 +573,7 @@ class expression(object): records = left_model.sudo().browse(ids) while records: parent_ids.update(records._ids) - records = records[parent_name] + records = records[parent_name] - records.browse(parent_ids) domain = [('id', 'in', list(parent_ids))] if prefix: return [(left, 'in', left_model._search(domain, order='id'))] -- GitLab