closes odoo/odoo#87527 Related: odoo/upgrade#3497 Related: odoo/enterprise#26939 Signed-off-by: Raphael Collet <rco@odoo.com>
442 lines
24 KiB
Python
442 lines
24 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)
|
|
|
|
# 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
|
|
|
|
@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)
|
|
|
|
# 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)
|
|
|
|
@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'))
|
|
)
|