# -*- 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.addons.mail.tests.common import mail_new_test_user from odoo.tests.common import tagged, users from odoo.tools import mute_logger @tagged('knowledge_acl') class TestKnowledgeSecurity(KnowledgeArticlePermissionsCase): """ Tests ACLs and low level access on models. Do not test the internals of permission computation as those are done in another test suite. Here we rely on them to check the create/read/write/unlink access checks. """ @classmethod def setUpClass(cls): """ Add some test users for security / groups check """ super().setUpClass() cls.user_erp_manager = mail_new_test_user( cls.env, company_id=cls.company_admin.id, country_id=cls.env.ref('base.be').id, groups='base.group_erp_manager', login='user_erp_manager', name='Emmanuel Erp Manager', notification_type='inbox', signature='--\nEmmanuel' ) @mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule') @users('user_public') def test_models_as_public(self): # ARTICLE with self.assertRaises(exceptions.AccessError, msg='ACLs: No article access to public'): self.env['knowledge.article'].search([]) # FAVORITES with self.assertRaises(exceptions.AccessError, msg='ACLs: No favorite access to public'): self.env['knowledge.article.favorite'].search([]) # MEMBERS with self.assertRaises(exceptions.AccessError, msg='ACLs: No member access to public'): self.env['knowledge.article.member'].search([]) # COVERS with self.assertRaises(exceptions.AccessError, msg='ACLs: no cover access to public'): self.env['knowledge.cover'].search([]) @mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule') @users('portal_test') def test_models_as_portal(self): article_root = self.article_roots[0].with_env(self.env) # ARTICLES with self.assertRaises(exceptions.AccessError, msg="ACLs: No access given to portal"): article_root.body # access body should trigger acls article_shared = self.article_roots[2].with_env(self.env) with self.assertRaises(exceptions.AccessError, msg="ACLs: Internal permission 'none', not for portal"): article_shared.body # access body should trigger acls article_accessible = self.article_write_contents[2].with_env(self.env) self.assertEqual(article_accessible.body, '

Writable Subarticle through inheritance

', "ACLs: should be accessible due to explicit 'read' member permission") self.assertTrue(article_accessible.is_user_favorite) # FAVORITES favs = self.env['knowledge.article.favorite'].search([]) self.assertEqual(len(favs), 1) self.assertEqual(favs.article_id, article_accessible) sudo_favorites = self.article_roots.favorite_ids.with_env(self.env) self.assertEqual(len(sudo_favorites), 2) with self.assertRaises(exceptions.AccessError, msg='ACLs: Breaking rule for portal'): sudo_favorites.mapped('user_id') # access body should trigger acls # MEMBERS my_members = self.env['knowledge.article.member'].search([]) self.assertEqual(len(my_members), 4) self.assertEqual( my_members, ( self.article_read_contents[0] | self.article_read_contents[1] | self.article_write_contents[2] ).article_member_ids, msg="Portal can read all members from articles he has access to" ) sudo_members = self.article_roots.article_member_ids.with_env(self.env) with self.assertRaises(exceptions.AccessError, msg='Breaking rule for portal'): sudo_members.mapped('partner_id') # access body should trigger acls # COVERS with self.assertRaises(exceptions.AccessError, msg="ACLs: No cover access to portal"): self.env['knowledge.cover'].search([]) @mute_logger('odoo.models.unlink') @users('user_erp_manager') def test_models_as_erp_manager(self): self.assertTrue(self.env.user.has_group('base.group_erp_manager')) self.assertFalse(self.env.user.has_group('base.group_system')) article_writable = self.article_roots[0].with_env(self.env) article_writable.body # access body should trigger acls article_readable = self.article_roots[1].with_env(self.env) article_readable.body # access body should trigger acls self.assertTrue(article_readable.user_has_access) self.assertTrue(article_readable.user_can_read) self.assertFalse(article_readable.user_has_write_access) self.assertFalse(article_readable.user_can_write) # ARTICLE: CREATE: cannot create a private article for another user with self.assertRaises(exceptions.AccessError, msg='Erp Managers behave like internal users'): _other_private = self.env['knowledge.article'].create({ 'article_member_ids': [(0, 0, { 'partner_id': self.partner_employee.id, 'permission': 'write', })], 'internal_permission': 'none', 'name': 'Private for Employee', }) @mute_logger('odoo.models.unlink') @users('admin') def test_models_as_system(self): self.assertTrue(self.env.user.has_group('base.group_system')) article_roots = self.article_roots.with_env(self.env) article_roots.mapped('body') # access body should trigger acls article_hidden = self.article_read_contents[3].with_env(self.env) article_hidden.body # access body should trigger acls article_readable = self.article_roots[1].with_env(self.env) article_readable.body # access body should trigger acls self.assertTrue(article_readable.user_has_access) self.assertTrue(article_readable.user_can_read) self.assertFalse(article_readable.user_has_write_access) self.assertTrue(article_readable.user_can_write) # ARTICLE: CREATE/READ # create a private article for another user other_private = self.env['knowledge.article'].create({ 'article_member_ids': [(0, 0, { 'partner_id': self.partner_employee.id, 'permission': 'write', })], 'internal_permission': 'none', 'name': 'Private for Employee', }) self.assertMembers(other_private, 'none', {self.partner_employee: 'write'}) self.assertEqual(other_private.category, 'private') self.assertTrue(other_private.user_can_write, 'Can write ACL-like is True, system can do everything') self.assertFalse(other_private.user_has_write_access, 'Can write based on permission is False but can perform write due to ACLs') other_private.write({'name': 'Admin can do everything'}) # create a child to it other_private_child = self.env['knowledge.article'].create({ 'name': 'Child of Private for Employee', 'parent_id': other_private.id, }) self.assertMembers(other_private_child, False, {}) self.assertEqual(other_private_child.article_member_ids.partner_id, self.env['res.partner']) self.assertEqual(other_private_child.category, 'private') self.assertTrue(other_private_child.user_can_write, 'Can write ACL-like is True, system can do everything') self.assertFalse(other_private_child.user_has_write_access, 'Can write based on permission is False but can perform write due to ACLs') # ARTICLE: WRITE other_private.write({'name': 'Can Update'}) other_private_child.write({'name': 'Can Also Update'}) # FAVORITES: CREATE/READ/UNLINK other_private_child.action_toggle_favorite() self.assertTrue(other_private_child.is_user_favorite) favorite_rec = self.env['knowledge.article.favorite'].search([('article_id', '=', other_private_child.id)]) favorite_rec.unlink() self.assertFalse(other_private_child.is_user_favorite) # MEMBERS: CREATE/READ/UNLINK members = other_private.article_member_ids self.assertEqual(members.partner_id, self.partner_employee) new_member = self.env['knowledge.article.member'].create({ 'article_id': other_private.id, 'partner_id': self.partner_employee2.id, 'permission': 'read', }) members = other_private.article_member_ids self.assertEqual(members.partner_id, self.partner_employee + self.partner_employee2) new_member.write({'permission': 'write'}) members.filtered(lambda m: m.partner_id == self.partner_employee).unlink() members = other_private.article_member_ids self.assertEqual(members, new_member) self.assertEqual(members.partner_id, self.partner_employee2) # COVERS cover = self._create_cover() cover.write({'attachment_url': '/'}) self.assertEqual(cover.attachment_url, '/') cover.unlink() @mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule', 'odoo.models.unlink') @users('employee') def test_models_as_user(self): article_roots = self.article_roots.with_env(self.env) # ARTICLES article_roots.mapped('body') # access body should trigger acls article_roots[0].write({'name': 'Hacked (or not)'}) with self.assertRaises(exceptions.AccessError, msg="ACLs: 'read' internal permission"): article_roots[1].write({'name': 'Hacked'}) with self.assertRaises(exceptions.AccessError, msg="ACLs: 'read' member permission"): article_roots[2].write({'name': 'Hacked'}) article_hidden = self.article_read_contents[3].with_env(self.env) with self.assertRaises(exceptions.AccessError, msg="ACLs: 'none' internal permission"): article_hidden.body # access body should trigger acls # FAVORITES my_favs = self.env['knowledge.article.favorite'].search([]) self.assertEqual( my_favs, self.articles_all.favorite_ids.filtered(lambda f: f.user_id == self.env.user), 'Favorites: employee should see its own favorites' ) my_favs.mapped('user_id') # access body should trigger acls my_favs.write({'sequence': 0}) with self.assertRaises(exceptions.AccessError, msg="ACLs: should not be used to change article/user"): my_favs[0].write({'article_id': article_roots[0].id}) with self.assertRaises(exceptions.AccessError, msg="ACLs: should not be used to change article/user"): my_favs[0].write({'user_id': self.user_portal.id}) # MEMBERS my_members = self.env['knowledge.article.member'].search([('article_id', 'in', self.article_roots.ids)]) self.assertEqual(len(my_members), 4) self.assertEqual( my_members, self.article_roots.article_member_ids, 'Members: employee should memberships of visible ' ) # remove employee from Shared root, check they cannot read those members self.article_roots[2].article_member_ids.filtered(lambda m: m.partner_id == self.partner_employee).unlink() my_members = self.env['knowledge.article.member'].search([('article_id', 'in', self.article_roots.ids)]) self.assertEqual(len(my_members), 2) self.assertEqual( my_members, (self.article_roots[1] + self.article_roots[3]).article_member_ids, 'Members: employee should see its own memberships' ) my_members.mapped('partner_id') # access body should trigger acls with self.assertRaises(exceptions.AccessError, msg="ACLs: no ACLs for write for user"): my_members.write({'permission': 'write'}) # COVERS cover = self._create_cover() cover.write({'attachment_url': '/'}) self.assertEqual(cover.attachment_url, '/') cover.unlink() @mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule') @users('employee') def test_models_as_user_copy(self): article_hidden = self.article_read_contents[3].with_env(self.env) with self.assertRaises(exceptions.AccessError, msg="ACLs: 'none' internal permission"): article_hidden.body # access body should trigger acls with self.assertRaises(exceptions.AccessError, msg="ACLs: copy should not allow to access hidden articles"): _new_article = article_hidden.copy() article_root_readonly = self.article_roots[0].with_env(self.env) with self.assertRaises(exceptions.AccessError, msg="ACLs: copy should not allow to duplicate other people members"): _new_article = article_root_readonly.copy()