Files
odoo_source/addons/knowledge/tests/test_knowledge_article_permissions.py
T
Adrien Schoffeniels ebddc1484b [FIX] knowledge: fix inheritance of internal permissions
Description of the issue:
=========================

Suppose we have 3 articles with the following structure:

root
   L child
         L grand child

When restricting the internal permission in 'child', the inherited
permission parent id and the inherited permission of 'grand child'
are not updated, and the share panel shows that the internal
permission of grand child is based on 'root'.
The 2 fields of grand child (and of any other child of 'child' at any
depth) should be recomputed, and the share panel should show that
the internal permission of these children is based on 'child'.

Solution:
=========
Add 'parent_id.inherited_permission_parent_id' as field on which
'_compute_inherited_permission' depends on, so that 'inherited
permission' and 'inherited permission parent id' of the children of
an article are recomputed when the internal permission of this
article changes.

The scenario described in the issue description has been added to the
tests. One now ensures that if an article gets its internal
permission downgraded (either to 'read' or 'none'), the child and the
grand-child of this article have their inherited permission parent id
equal to the article and their inherited permission equal to the
internal permission of the article.

Task-2866950

X-original-commit: ea2d1b64c5f30c6d0e6a704b4f09c43084371b62
Part-of: odoo/odoo#94834
2022-06-29 12:19:53 +02:00

460 lines
25 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import exceptions
from odoo.addons.knowledge.tests.common import KnowledgeArticlePermissionsCase
from odoo.tests.common import tagged, users
from odoo.tools import mute_logger
@tagged('knowledge_acl')
class TestKnowledgeArticlePermissions(KnowledgeArticlePermissionsCase):
@users('employee')
def test_article_main_parent(self):
""" Test root article computation """
article_roots = self.article_roots.with_env(self.env)
articles_write = (self.article_write_contents + self.article_write_contents_children).with_env(self.env)
self.assertEqual(articles_write.root_article_id, article_roots[0])
articles_write = self.article_read_contents.with_env(self.env)
self.assertEqual(articles_write.root_article_id, article_roots[1])
# desynchronized still have a root (do as sudo)
self.assertEqual(self.article_write_desync.root_article_id, article_roots[0])
self.assertEqual(self.article_read_desync.root_article_id, article_roots[1])
def test_article_permissions_desync(self):
""" Test computed fields based on permissions (independently from ACLs
aka not user_permission, ...). Main use cases: desynchronized articles
or articles without parents. """
for (exp_inherited_permission,
exp_inherited_permission_parent_id,
exp_internal_permission
), article in zip(
[('read', self.env['knowledge.article'], 'read'),
('read', self.article_write_desync[0], False),
('none', self.env['knowledge.article'], 'none'),
('none', self.article_read_desync[0], False),
('write', self.env['knowledge.article'], 'write'),
('read', self.env['knowledge.article'], 'read'),
],
self.article_write_desync + self.article_read_desync + self.article_roots
):
self.assertEqual(article.inherited_permission, exp_inherited_permission,
f'Permission: wrong inherit computation for {article.name}: {article.inherited_permission} instead of {exp_inherited_permission}')
self.assertEqual(article.inherited_permission_parent_id, exp_inherited_permission_parent_id,
f'Permission: wrong inherit computation for {article.name}: {article.inherited_permission_parent_id.name} instead of {exp_inherited_permission_parent_id.name}')
self.assertEqual(article.internal_permission, exp_internal_permission,
f'Permission: wrong inherit computation for {article.name}: {article.internal_permission} instead of {exp_internal_permission}')
@mute_logger('odoo.addons.base.models.ir_rule')
def test_article_permissions_inheritance_desync(self):
""" Test desynchronize (and therefore member propagation that should be
stopped). """
article_desync = self.article_write_desync[0]
self.assertMembers(article_desync, 'read', {self.partner_employee_manager: 'write'})
# as employee w write perms
article_desync = article_desync.with_user(self.user_employee_manager)
self.assertTrue(article_desync.user_has_write_access)
self.assertTrue(article_desync.user_has_access)
# as employee
article_desync = article_desync.with_user(self.user_employee)
self.assertFalse(article_desync.user_has_write_access)
self.assertTrue(article_desync.user_has_access)
# as portal
article_desync = article_desync.with_user(self.user_portal)
self.assertFalse(article_desync.user_has_write_access)
self.assertFalse(article_desync.user_has_access, 'Permissions: member rights should not be fetch on parents')
@mute_logger('odoo.addons.base.models.ir_rule')
@users('employee')
def test_article_permissions_inheritance_employee(self):
article_roots = self.article_roots.with_env(self.env)
# roots: based on internal permissions
self.assertEqual(article_roots.mapped('user_has_write_access'), [True, False, False, True])
self.assertEqual(article_roots.mapped('user_has_access'), [True, True, True, True])
self.assertEqual(article_roots.mapped('user_permission'), ['write', 'read', 'read', 'write'])
# write permission from ancestors
article_write_ancestor = self.article_write_contents[2].with_env(self.env)
self.assertEqual(article_write_ancestor.inherited_permission, 'write')
self.assertEqual(article_write_ancestor.inherited_permission_parent_id, self.article_roots[0])
self.assertFalse(article_write_ancestor.internal_permission)
self.assertEqual(article_write_ancestor.user_permission, 'write')
# write permission from ancestors overridden by internal permission
article_read_forced = self.article_write_contents[1].with_env(self.env)
self.assertEqual(article_read_forced.inherited_permission, 'read')
self.assertFalse(article_read_forced.inherited_permission_parent_id)
self.assertEqual(article_read_forced.internal_permission, 'read')
self.assertEqual(article_read_forced.user_permission, 'read')
# write permission from ancestors overridden by member permission
article_read_member = self.article_write_contents[0].with_env(self.env)
self.assertEqual(article_read_member.inherited_permission, 'write')
self.assertEqual(article_read_member.inherited_permission_parent_id, self.article_roots[0])
self.assertFalse(article_read_member.internal_permission)
self.assertEqual(article_read_member.user_permission, 'read')
# forced lower than base article perm (see 'Community Paranoïa')
article_lower = self.article_read_contents[1].with_env(self.env)
self.assertEqual(article_lower.inherited_permission, 'write')
self.assertFalse(article_lower.inherited_permission_parent_id)
self.assertEqual(article_lower.internal_permission, 'write')
self.assertEqual(article_lower.user_permission, 'read')
# read permission from ancestors
article_read_ancestor = self.article_read_contents[2].with_env(self.env)
self.assertEqual(article_read_ancestor.inherited_permission, 'read')
self.assertEqual(article_read_ancestor.inherited_permission_parent_id, self.article_roots[1])
self.assertFalse(article_read_ancestor.internal_permission)
self.assertEqual(article_read_ancestor.user_permission, 'read')
# permission denied
article_none = self.article_read_contents[3].with_env(self.env)
with self.assertRaises(exceptions.AccessError):
article_none.name
@mute_logger('odoo.addons.base.models.ir_rule')
@users('portal_test')
def test_article_permissions_inheritance_portal(self):
article_roots = self.article_roots.with_env(self.env)
with self.assertRaises(exceptions.AccessError):
article_roots.mapped('internal_permission')
article_members = self.article_read_contents[0:2].with_env(self.env)
self.assertEqual(article_members.mapped('inherited_permission'), ['write', 'write']) # TDE: TOCHECK
self.assertEqual(article_members.mapped('internal_permission'), ['write', 'write']) # TDE: TOCHECK
self.assertEqual(article_members.mapped('user_has_write_access'), [False, False], 'Portal: can never write')
self.assertEqual(article_members.mapped('user_has_access'), [True, True], 'Portal: access through membership')
self.assertEqual(article_members.mapped('user_permission'), ['read', 'read'])
@users('employee')
def test_article_permissions_employee_new_mode(self):
""" Test transient / cache mode: computed fields without IDs, ... """
article = self.env['knowledge.article'].new({'name': 'Transient'})
self.assertFalse(article.inherited_permission)
self.assertFalse(article.internal_permission)
self.assertTrue(article.user_has_write_access)
self.assertTrue(article.user_has_access)
self.assertEqual(article.user_permission, 'write')
@tagged('knowledge_internals', 'knowledge_management')
class KnowledgeArticlePermissionsInitialValues(KnowledgeArticlePermissionsCase):
""" Test initial values or our test data once so that other tests do not have
to do it. """
def test_initial_values(self):
article_roots = self.article_roots.with_env(self.env)
article_headers = self.article_headers.with_env(self.env)
# roots: defaults on write, inherited = internal
self.assertEqual(article_roots.mapped('inherited_permission'), ['write', 'read', 'none', 'none'])
self.assertFalse(article_roots.inherited_permission_parent_id)
self.assertEqual(article_roots.mapped('internal_permission'), ['write', 'read', 'none', 'none'])
# childs: allow void permission, inherited = go up to first defined permission
self.assertEqual(article_headers.mapped('inherited_permission'), ['write', 'read', 'read'])
self.assertEqual(
[p.inherited_permission_parent_id for p in article_headers],
[article_roots[0], article_roots[1], article_roots[1]]
)
self.assertEqual(article_headers.mapped('internal_permission'), [False, False, False])
@users('employee')
def test_initial_values_as_employee(self):
""" Ensure all tests have the same basis (user specific computed as
employee for acl-dependent tests) """
article_write_inherit = self.article_write_contents[2].with_env(self.env)
# initial values: write through inheritance
self.assertMembers(article_write_inherit, False, {self.partner_portal: 'read'})
self.assertFalse(article_write_inherit.internal_permission)
self.assertFalse(article_write_inherit.is_desynchronized)
self.assertTrue(article_write_inherit.user_has_write_access)
self.assertTrue(article_write_inherit.user_has_access)
article_write_inherit_as2 = article_write_inherit.with_user(self.user_employee2)
self.assertTrue(article_write_inherit_as2.user_has_write_access)
self.assertTrue(article_write_inherit_as2.user_has_access)
@tagged('knowledge_acl')
class TestKnowledgeArticlePermissionsTools(KnowledgeArticlePermissionsCase):
@mute_logger('odoo.addons.base.models.ir_rule')
@users('employee')
def test_downgrade_internal_permission_none(self):
writable_as1 = self.article_write_contents[2].with_env(self.env)
writable_as2 = writable_as1.with_user(self.user_employee2)
self.assertEqual(writable_as2.user_has_access, True)
writable_root = self.article_roots[0].with_env(self.env)
writable_children = self.article_write_contents_children.with_env(self.env)
for child in writable_children:
self.assertEqual(child.inherited_permission_parent_id, writable_root)
# downgrade write global perm to read
writable_as1._set_internal_permission('none')
writable_as1.flush_model() # ACLs are done using SQL
self.assertMembers(
writable_as1, 'none',
{self.partner_portal: 'read', # untouched by downgrade
self.env.user.partner_id: 'write'},
'Permission: lowering permission adds current user in members to have write access'
)
self.assertTrue(writable_as1.is_desynchronized)
self.assertTrue(writable_as1.user_has_write_access)
self.assertTrue(writable_as1.user_has_access)
# check internal permission has been lowered
with self.assertRaises(exceptions.AccessError):
writable_as2.body # trigger ACLs
# check children inherits downgraded permissions from article
for child in writable_children:
self.assertEqual(child.inherited_permission, 'none', 'Permission: lowering permission should lower the permission of the children')
self.assertEqual(child.inherited_permission_parent_id, writable_as1, 'Permission: lowering permission should make the children inherit the permission from this article')
@users('employee')
def test_downgrade_internal_permission_read(self):
writable_as1 = self.article_write_contents[2].with_env(self.env)
writable_as2 = writable_as1.with_user(self.user_employee2)
self.assertEqual(writable_as2.user_has_access, True)
writable_root = self.article_roots[0].with_env(self.env)
writable_children = self.article_write_contents_children.with_env(self.env)
for child in writable_children:
self.assertEqual(child.inherited_permission_parent_id, writable_root)
# downgrade write global perm to read
writable_as1._set_internal_permission('read')
writable_as1.flush_model() # ACLs are done using SQL
self.assertMembers(
writable_as1, 'read',
{self.partner_portal: 'read', self.env.user.partner_id: 'write'},
'Permission: lowering permission adds current user in members to have write access'
)
self.assertTrue(writable_as1.is_desynchronized)
self.assertTrue(writable_as1.user_has_write_access)
self.assertTrue(writable_as1.user_has_access)
self.assertFalse(writable_as2.user_has_write_access)
self.assertTrue(writable_as2.user_has_access)
# check children inherits downgraded permissions from article
for child in writable_children:
self.assertEqual(child.inherited_permission, 'read', 'Permission: lowering permission should lower the permission of the children')
self.assertEqual(child.inherited_permission_parent_id, writable_as1, 'Permission: lowering permission should make the children inherit the permission from this article')
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.models.unlink')
@users('employee')
def test_remove_member_inherited_rights(self):
""" Remove a member from a child inheriting rights: will desync """
writable = self.article_write_contents[2].with_env(self.env)
self.assertTrue(writable.user_has_access)
self.assertTrue(writable.user_has_write_access)
self.assertMembers(writable, False,
{self.partner_portal: 'read'})
# set partner employee manager as writable member of its root
writable_root = writable.root_article_id
writable_root._add_members(self.partner_employee_manager, 'write')
self.assertMembers(writable_root, 'write',
{self.partner_employee_manager: 'write'})
# remove partner employee manager that has rights based on inheritance
manager_member = writable_root.article_member_ids.filtered(lambda m: m.partner_id == self.partner_employee_manager)
writable._remove_member(manager_member)
self.assertTrue(writable.is_desynchronized,
'Permission: when removing a member having inherited rights it has be be desynchronized')
self.assertMembers(writable, 'write',
{self.partner_portal: 'read'})
# resync
writable.restore_article_access()
self.assertFalse(writable.is_desynchronized)
self.assertMembers(writable, False,
{self.partner_portal: 'read'})
# remove portal partner that has rights based on membership
portal_member = writable.article_member_ids.filtered(lambda m: m.partner_id == self.partner_portal)
writable._remove_member(portal_member)
self.assertFalse(writable.is_desynchronized)
self.assertMembers(writable, False, {})
@mute_logger('odoo.models.unlink')
@users('employee')
def test_set_member_permission(self):
""" Test setting member-specific permission """
writable = self.article_write_contents[2].with_env(self.env)
self.assertTrue(writable.user_has_access)
self.assertTrue(writable.user_has_write_access)
# set partner employee manager as readable member of its root
writable_root = writable.root_article_id
writable_root._add_members(self.partner_employee_manager, 'read')
self.assertMembers(writable_root, 'write',
{self.partner_employee_manager: 'read'})
# update a member permission directly
portal_member = writable.article_member_ids.filtered(lambda m: m.partner_id == self.partner_portal)
writable._set_member_permission(portal_member, 'none')
self.assertMembers(writable, False,
{self.partner_portal: 'none'})
# upgrade a permission based on inheritance
manager_member_root = writable_root.article_member_ids.filtered(lambda m: m.partner_id == self.partner_employee_manager)
writable._set_member_permission(manager_member_root, 'write', is_based_on=True)
self.assertFalse(writable.is_desynchronized)
self.assertMembers(writable, False,
{self.partner_portal: 'none',
self.partner_employee_manager: 'write'})
# now test downgrading
manager_member = writable.article_member_ids.filtered(lambda m: m.partner_id == self.partner_employee_manager)
writable_root._set_member_permission(manager_member_root, 'write')
writable._remove_member(manager_member)
self.assertMembers(writable_root, 'write',
{self.partner_employee_manager: 'write'})
self.assertMembers(writable, False,
{self.partner_portal: 'none'})
# downgrade a permission, should desynchronize from parent
writable._set_member_permission(manager_member_root, 'read', is_based_on=True)
self.assertTrue(writable.is_desynchronized,
'Permission: when removing a member having inherited rights it has be be desynchronized')
self.assertMembers(writable, 'write',
{self.partner_portal: 'none',
self.partner_employee_manager: 'read'})
@mute_logger('odoo.addons.base.models.ir_rule')
@users('employee')
def test_update_internal_permission_escalation(self):
""" Check no privilege escalation is possible """
# direct try at setting higher internal permission
readonly = self.article_read_contents[1].with_env(self.env)
self.assertTrue(readonly.user_has_access)
self.assertFalse(readonly.user_has_write_access)
writable = self.article_write_contents[2].with_env(self.env)
self.assertTrue(writable.user_has_access)
self.assertTrue(writable.user_has_write_access)
with self.assertRaises(exceptions.AccessError,
msg='Permission: that is plain stupid trying to do this'):
readonly.write({'internal_permission': 'write'})
with self.assertRaises(exceptions.AccessError,
msg='Permission: do not allow privilege escalation'):
readonly._set_internal_permission('write')
portal_member = writable.article_member_ids.filtered(lambda m: m.partner_id == self.partner_portal)
with self.assertRaises(exceptions.ValidationError,
msg='Permission: share partner cannot gain write access'):
writable._set_member_permission(portal_member, 'write')
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.models.unlink')
@users('employee')
def test_update_permissions_rights(self):
""" Check no privilege escalation is possible """
# direct try at setting higher internal permission
readonly = self.article_read_contents[1].with_env(self.env)
self.assertTrue(readonly.user_has_access)
self.assertFalse(readonly.user_has_write_access)
with self.assertRaises(exceptions.AccessError,
msg='Permission: that is plain stupid trying to do this'):
readonly.write({'internal_permission': 'write'})
with self.assertRaises(exceptions.AccessError,
msg='Permission: do not allow privilege escalation'):
readonly._set_internal_permission('write')
other_member = readonly.article_member_ids.filtered(lambda m: m.partner_id == self.partner_portal)
with self.assertRaises(exceptions.AccessError,
msg='Permission: do not allow to remove members when having only read access'):
readonly._remove_member(other_member)
self.assertMembers(readonly, 'write',
{self.env.user.partner_id: 'read',
self.partner_portal: 'read'})
# cannot gain privilege: setting none if I remove myself to ensure no gain is performed
my_member = readonly.article_member_ids.filtered(lambda m: m.partner_id == self.env.user.partner_id)
readonly._remove_member(my_member)
self.assertMembers(readonly, 'write',
{self.env.user.partner_id: 'none',
self.partner_portal: 'read'})
@tagged('knowledge_acl')
class TestKnowledgeArticleSearch(KnowledgeArticlePermissionsCase):
@users('admin')
def test_article_search_admin(self):
""" Test admin: can read / write everything but user_has_access and
user_has_write_access should still be based on real permissions. """
self.assertTrue(self.env.user.has_group('base.group_system'))
articles = self.env['knowledge.article'].search([])
expected = self.articles_all
self.assertEqual(articles, expected,
'Search on user_has_write_access: aka write access (additional: %s, missing: %s)' %
((articles - expected).mapped('name'), (expected - articles).mapped('name'))
)
articles = self.env['knowledge.article'].search([('user_has_write_access', '=', True)])
expected = self.article_roots[0] + self.article_headers[0] + \
self.article_write_contents[0] + self.article_write_contents[2] + \
self.article_write_contents_children + \
self.article_read_contents[0:2]
self.assertEqual(articles, expected,
'Search on user_has_write_access: aka write access (additional: %s, missing: %s)' %
((articles - expected).mapped('name'), (expected - articles).mapped('name'))
)
@users('employee')
def test_article_search_employee(self):
""" Test regular searches using permission-based ACLs """
# explicitly remove an article, check it is not included (nor its child)
self.article_write_desync[0].write({
'article_member_ids': [
(0, 0, {'partner_id': self.user_employee.partner_id.id,
'permission': 'none'})]
})
articles = self.env['knowledge.article'].search([])
# not reachable: 'none', desynchronized 'none' (and their children)
expected = self.articles_all - self.article_read_contents[3] - self.article_write_desync
self.assertEqual(articles, expected,
'Search on main article: aka everything except "none"-based articles (additional: %s, missing: %s)' %
((articles - expected).mapped('name'), (expected - articles).mapped('name'))
)
# add its child as readable through membership and perform a new search
self.article_write_desync[1].write({
'article_member_ids': [
(0, 0, {'partner_id': self.user_employee.partner_id.id,
'permission': 'read'})]
})
articles = self.env['knowledge.article'].search([('root_article_id', '=', self.article_roots[0].id)])
expected = self.article_roots[0] + self.article_headers[0] + \
self.article_write_contents + self.article_write_contents_children + self.article_write_desync[1]
self.assertEqual(articles, expected,
'Search on main article: aka read access on read root + its children (additional: %s, missing: %s)' %
((articles - expected).mapped('name'), (expected - articles).mapped('name'))
)
@users('employee')
def test_article_search_employee_method_based(self):
""" Test search methods """
articles = self.env['knowledge.article'].search([('user_has_write_access', '=', True)])
expected = self.article_roots[0] + self.article_roots[3] + \
self.article_headers[0] + \
self.article_write_contents[2] + self.article_write_contents_children + \
self.article_read_contents[0] + self.article_read_desync
self.assertEqual(articles, expected,
'Search on user_has_write_access: aka write access (additional: %s, missing: %s)' %
((articles - expected).mapped('name'), (expected - articles).mapped('name'))
)