[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#123353 X-original-commit: 2e1adc0c3e33fcf7989d27bb4d1c2e3c019faf2b Signed-off-by: Ivan Elizaryev (iel) <iel@odoo.com> Signed-off-by: Raphael Collet <rco@odoo.com>
This commit is contained in:
committed by
Ivan Rasputin
parent
afbf556a6a
commit
101acf3773
@@ -1783,3 +1783,21 @@ class EmptyChar(models.Model):
|
||||
_description = 'A model to test emtpy char'
|
||||
|
||||
name = fields.Char('Name')
|
||||
|
||||
|
||||
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')
|
||||
|
||||
@@ -108,3 +108,5 @@ access_test_new_api_lesson,access_test_new_api_lesson,model_test_new_api_lesson,
|
||||
access_test_new_api_person,access_test_new_api_person,model_test_new_api_person,base.group_user,1,1,1,1
|
||||
access_test_new_api_employer,access_test_new_api_employer,model_test_new_api_employer,base.group_user,1,1,1,1
|
||||
public_access_test_new_api_course,access_test_new_api_course,model_test_new_api_course,base.group_public,1,0,0,0
|
||||
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
|
||||
|
||||
|
@@ -409,3 +409,28 @@ class One2manyCase(TransactionCase):
|
||||
self.assertEqual(len(parent.child_ids), 3)
|
||||
self.assertEqual(parent, parent.child_ids.parent_id)
|
||||
self.assertEqual(parent.child_ids.mapped('name'), ['C3', 'PO', 'R2D2'])
|
||||
|
||||
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)])
|
||||
|
||||
@@ -878,11 +878,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))]
|
||||
@@ -910,7 +912,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))]
|
||||
|
||||
Reference in New Issue
Block a user