Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
145
directive_layer/tests/test_directive_security.py
Normal file
145
directive_layer/tests/test_directive_security.py
Normal file
@@ -0,0 +1,145 @@
|
||||
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.",
|
||||
)
|
||||
Reference in New Issue
Block a user