from odoo.exceptions import AccessError from odoo.tests.common import TransactionCase, tagged @tagged('standard', 'at_install', 'directive_logic') class TestDirectiveSecurity(TransactionCase): """Поведенческая проверка ролевой модели безопасности (design 5.1/5.2). Назначаем бизнес-роль через role_line_ids и проверяем, что: - пользователь попадает в техническую группу _res_groups (implied_ids); - исполнитель читает/двигает свою директиву, но не может её удалить; - исполнитель не видит чужие директивы (record rule rule_directive_own); - диспетчер видит все директивы и может удалять. """ def setUp(self): super().setUp() self.role_executor = self.env.ref('directive_layer.role_directive_executor') self.role_dispatcher = self.env.ref('directive_layer.role_directive_dispatcher') self.grp_executor = self.env.ref('directive_layer.role_directive_executor_res_groups') self.grp_dispatcher = self.env.ref('directive_layer.role_directive_dispatcher_res_groups') self.work_type = self.env['directive.work.type'].create({ 'name': 'Development', 'code': 'dev', }) self.stage_draft = self.env['directive.stage'].search([('code', '=', 'draft')], limit=1) if not self.stage_draft: self.stage_draft = self.env['directive.stage'].create({'name': 'Draft', 'code': 'draft'}) self.executor_user = self._make_user('exec_user', self.role_executor) self.dispatcher_user = self._make_user('disp_user', self.role_dispatcher) # Рядовой internal user без единой роли директив (fail-closed). self.roleless_user = self.env['res.users'].create({ 'name': 'roleless_user', 'login': 'roleless_user', 'email': 'roleless_user@example.com', 'group_ids': [(4, self.env.ref('base.group_user').id)], }) # Своя директива исполнителя + чужая директива. self.own_directive = self._make_directive( 'Own directive', executor_user=self.executor_user) self.other_directive = self._make_directive( 'Other directive', executor_user=self.dispatcher_user) def _make_user(self, login, role): # На create res.users вызывается set_groups_from_roles(): группы # пользователя заменяются на role.group_id + implied_ids (т.е. _res_groups). return self.env['res.users'].create({ 'name': login, 'login': login, 'email': f'{login}@example.com', 'role_line_ids': [(0, 0, {'role_id': role.id})], }) def _make_directive(self, name, executor_user): origin = self.env['directive.origin'].create({ 'res_model': 'res.partner', 'res_id': self.env.user.partner_id.id, }) return self.env['directive.directive'].create({ 'name': name, 'deadline_at': '2099-01-01 00:00:00', 'planned_hours': 1.0, 'work_type_id': self.work_type.id, 'stage_id': self.stage_draft.id, 'origin_id': origin.id, 'executor_user_id': executor_user.id, }) def test_01_role_grants_technical_group(self): """Назначение бизнес-роли переводит пользователя в её _res_groups.""" self.assertTrue( self.executor_user.has_group('directive_layer.role_directive_executor_res_groups'), "Исполнитель не попал в role_directive_executor_res_groups после назначения роли.", ) self.assertTrue( self.dispatcher_user.has_group('directive_layer.role_directive_dispatcher_res_groups'), "Диспетчер не попал в role_directive_dispatcher_res_groups после назначения роли.", ) def test_02_executor_read_write_own(self): """Исполнитель читает и двигает свою директиву.""" directive = self.own_directive.with_user(self.executor_user) self.assertEqual(directive.name, 'Own directive') directive.write({'load_percent': 50}) self.assertEqual(directive.load_percent, 50) def test_03_executor_cannot_unlink(self): """Исполнитель не может удалить директиву (нет perm_unlink).""" directive = self.own_directive.with_user(self.executor_user) with self.assertRaises(AccessError): directive.unlink() def test_04_executor_cannot_create(self): """Исполнитель не может создавать директивы напрямую (нет perm_create).""" origin = self.env['directive.origin'].create({ 'res_model': 'res.partner', 'res_id': self.env.user.partner_id.id, }) with self.assertRaises(AccessError): self.env['directive.directive'].with_user(self.executor_user).create({ 'name': 'Illegal', 'deadline_at': '2099-01-01 00:00:00', 'planned_hours': 1.0, 'work_type_id': self.work_type.id, 'stage_id': self.stage_draft.id, 'origin_id': origin.id, }) def test_05_executor_sees_only_own(self): """Record rule: исполнитель видит свою директиву, но не чужую.""" Directive = self.env['directive.directive'].with_user(self.executor_user) visible_ids = Directive.search([]).ids self.assertIn(self.own_directive.id, visible_ids, "Исполнитель не видит собственную директиву.") self.assertNotIn(self.other_directive.id, visible_ids, "Исполнитель видит чужую директиву — record rule не работает.") def test_06_dispatcher_sees_all_and_can_unlink(self): """Диспетчер видит все директивы и может удалять.""" Directive = self.env['directive.directive'].with_user(self.dispatcher_user) visible_ids = Directive.search([]).ids self.assertIn(self.own_directive.id, visible_ids) self.assertIn(self.other_directive.id, visible_ids) Directive.browse(self.own_directive.id).unlink() self.assertFalse(self.own_directive.exists()) def test_07_roleless_user_sees_zero(self): """Fail-closed: internal user без ролей директив видит ноль директив. base.group_user даёт только bounded read (чтобы related-поля на формах- источниках грузились), а global record rule ограничивает выборку нулём строк для пользователя без совпадений own/role. Эта проверка фиксирует инвариант, чтобы будущий рефактор не ослабил fail-closed молча. """ Directive = self.env['directive.directive'].with_user(self.roleless_user) self.assertFalse(self.roleless_user.has_group( 'directive_layer.role_directive_dispatcher_res_groups')) self.assertEqual( Directive.search([]).ids, [], "Пользователь без ролей директив видит директивы — нарушен fail-closed.", )