diff --git a/addons/project/models/project_task.py b/addons/project/models/project_task.py index 69234e827d7..4651c8c10dd 100644 --- a/addons/project/models/project_task.py +++ b/addons/project/models/project_task.py @@ -796,6 +796,17 @@ class Task(models.Model): error_message = _('You cannot write on %s fields in task.', ', '.join(unauthorized_fields)) raise AccessError(error_message) + def _get_sudo_portal_vals(self, vals): + """ returns the values which must be written without and with sudo when a portal user creates / writes a task. + :param vals: dict of {field: value}, the values to create/write + :return: a tuple with 2 dicts: + - the first with the values to write without sudo + - the second with the values to write with sudo + """ + vals_no_sudo = {key: val for key, val in vals.items() if self._fields[key].type in ('one2many', 'many2many')} + vals_sudo = {key: val for key, val in vals.items() if key not in vals_no_sudo} + return vals_no_sudo, vals_sudo + def read(self, fields=None, load='_classic_read'): self._ensure_fields_are_accessible(fields) return super(Task, self).read(fields=fields, load=load) @@ -885,6 +896,14 @@ class Task(models.Model): user_ids = self._fields['user_ids'].convert_to_cache(vals.get('user_ids', []), self) if self.env.user.id not in list(user_ids) + [SUPERUSER_ID]: vals['user_ids'] = [Command.set(list(user_ids) + [self.env.user.id])] + + if default_personal_stage and 'personal_stage_type_id' not in vals: + vals['personal_stage_type_id'] = default_personal_stage[0] + if not vals.get('name') and vals.get('display_name'): + vals['name'] = vals['display_name'] + if is_portal_user: + self._ensure_fields_are_accessible(vals.keys(), operation='write', check_group_user=False) + if project_id: # set the project => "I want to display the task in the project" # => => set `display_in_project` to True @@ -900,13 +919,6 @@ class Task(models.Model): 'display_in_project': False, }) - if default_personal_stage and 'personal_stage_type_id' not in vals: - vals['personal_stage_type_id'] = default_personal_stage[0] - if not vals.get('name') and vals.get('display_name'): - vals['name'] = vals['display_name'] - if is_portal_user: - self._ensure_fields_are_accessible(vals.keys(), operation='write', check_group_user=False) - if project_id and not "company_id" in vals: vals["company_id"] = self.env["project.project"].browse( project_id @@ -940,7 +952,11 @@ class Task(models.Model): was_in_sudo = self.env.su if is_portal_user: self = self.sudo().with_context(self._get_portal_sudo_context()) + vals_list_no_sudo, vals_list = zip(*(self._get_sudo_portal_vals(vals) for vals in vals_list)) tasks = super(Task, self.with_context(mail_create_nosubscribe=True)).create(vals_list) + if is_portal_user: + for task, vals in zip(tasks, vals_list_no_sudo): + task.sudo(was_in_sudo).write(vals) tasks._populate_missing_personal_stages() self._task_message_auto_subscribe_notify({task: task.user_ids - self.env.user for task in tasks}) @@ -1086,7 +1102,8 @@ class Task(models.Model): # requires the write access on others models, as rating.rating # in order to keep the same name than the task. if portal_can_write: - self = self.sudo().with_context(self._get_portal_sudo_context()) + self_no_sudo, self = self, self.sudo().with_context(self._get_portal_sudo_context()) + vals_no_sudo, vals = self._get_sudo_portal_vals(vals) # Track user_ids to send assignment notifications old_user_ids = {t: t.user_ids for t in self.sudo()} @@ -1095,6 +1112,8 @@ class Task(models.Model): del vals['personal_stage_type_id'] result = super().write(vals) + if portal_can_write: + super(Task, self_no_sudo).write(vals_no_sudo) if 'user_ids' in vals: self._populate_missing_personal_stages() diff --git a/addons/project/tests/test_project_sharing.py b/addons/project/tests/test_project_sharing.py index dcf35b21867..6103c8b8978 100644 --- a/addons/project/tests/test_project_sharing.py +++ b/addons/project/tests/test_project_sharing.py @@ -4,6 +4,7 @@ from odoo import Command from odoo.osv import expression from odoo.exceptions import AccessError +from odoo.tools import mute_logger from odoo.tests import tagged from odoo.tests.common import Form @@ -42,6 +43,12 @@ class TestProjectSharingCommon(TestProjectCommon): }) cls.project_portal.message_subscribe(partner_ids=[cls.partner_portal.id]) + cls.project_no_collabo = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({ + 'name': 'No Collabo', + 'privacy_visibility': 'followers', + 'alias_name': 'project+nocollabo', + }) + cls.task_cow = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({ 'name': 'Cow UserTask', 'user_ids': cls.user_projectuser, @@ -52,6 +59,12 @@ class TestProjectSharingCommon(TestProjectCommon): 'user_ids': cls.user_projectuser, 'project_id': cls.project_portal.id, }) + cls.task_no_collabo = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({ + 'name': 'No Collabo Task', + 'project_id': cls.project_no_collabo.id, + }) + + cls.task_tag = cls.env['project.tags'].create({'name': 'Foo'}) cls.project_sharing_form_view_xml_id = 'project.project_sharing_project_task_view_form' @@ -101,6 +114,7 @@ class TestProjectSharing(TestProjectSharingCommon): self.project_portal.write({'collaborator_ids': [Command.create({'partner_id': self.user_portal.partner_id.id})]}) self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user can access to project sharing feature of the portal project.') + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule') def test_create_task_in_project_sharing(self): """ Test when portal user creates a task in project sharing views. @@ -111,9 +125,10 @@ class TestProjectSharing(TestProjectSharingCommon): 3) Give the 'edit' access mode to a portal user in a project and try to create task with this user. 3.1) Try to change the project of the new task with this user. """ + Task = self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id, 'default_user_ids': [(4, self.user_portal.id)]}) # 1) Give the 'read' access mode to a portal user in a project and try to create task with this user. with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: + with self.get_project_sharing_form_view(Task, self.user_portal) as form: form.name = 'Test' task = form.save() @@ -122,17 +137,56 @@ class TestProjectSharing(TestProjectSharingCommon): Command.create({'partner_id': self.user_portal.partner_id.id}), ], }) - with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id, 'default_user_ids': [(4, self.user_portal.id)]}), self.user_portal) as form: + with self.get_project_sharing_form_view(Task, self.user_portal) as form: form.name = 'Test' + with form.child_ids.new() as subtask_form: + subtask_form.name = 'Test Subtask' task = form.save() self.assertEqual(task.name, 'Test') self.assertEqual(task.project_id, self.project_portal) self.assertFalse(task.portal_user_names) + + # Check creating a sub-task while creating the parent task works as expected. + self.assertEqual(task.child_ids.name, 'Test Subtask') + self.assertEqual(task.child_ids.project_id, self.project_portal) + self.assertFalse(task.child_ids.portal_user_names, 'by default no user should be assigned to a subtask created by the portal user.') + self.assertFalse(task.child_ids.user_ids, 'No user should be assigned to the new subtask.') + # 3.1) Try to change the project of the new task with this user. with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."): form.project_id = self.project_cows task = form.save() + Task = Task.with_user(self.user_portal) + # Create/Update a forbidden task through child_ids + with self.assertRaisesRegex(AccessError, "You cannot write on color"): + Task.create({'name': 'foo', 'child_ids': [Command.create({'name': 'Foo', 'color': 1})]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + Task.create({'name': 'foo', 'child_ids': [Command.update(self.task_no_collabo.id, {'name': 'Foo'})]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + Task.create({'name': 'foo', 'child_ids': [Command.delete(self.task_no_collabo.id)]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + Task.create({'name': 'foo', 'child_ids': [Command.unlink(self.task_no_collabo.id)]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + Task.create({'name': 'foo', 'child_ids': [Command.link(self.task_no_collabo.id)]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + Task.create({'name': 'foo', 'child_ids': [Command.set([self.task_no_collabo.id])]}) + + # Create/update a tag through tag_ids + with self.assertRaisesRegex(AccessError, "not allowed to create 'Project Tags'"): + Task.create({'name': 'foo', 'tag_ids': [Command.create({'name': 'Bar'})]}) + with self.assertRaisesRegex(AccessError, "not allowed to modify 'Project Tags'"): + Task.create({'name': 'foo', 'tag_ids': [Command.update(self.task_tag.id, {'name': 'Bar'})]}) + with self.assertRaisesRegex(AccessError, "not allowed to delete 'Project Tags'"): + Task.create({'name': 'foo', 'tag_ids': [Command.delete(self.task_tag.id)]}) + + task = Task.create({'name': 'foo', 'tag_ids': [Command.link(self.task_tag.id)]}) + self.assertEqual(task.tag_ids, self.task_tag) + + Task.create({'name': 'foo', 'tag_ids': [Command.set([self.task_tag.id])]}) + self.assertEqual(task.tag_ids, self.task_tag) + + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule') def test_edit_task_in_project_sharing(self): """ Test when portal user creates a task in project sharing views. @@ -199,6 +253,42 @@ class TestProjectSharing(TestProjectSharingCommon): subtask_form.name = 'Test Subtask' self.assertEqual(len(task.child_ids), 2, 'Check 2 subtasks has correctly been created by the user portal.') + # Create/Update a forbidden task through child_ids + with self.assertRaisesRegex(AccessError, "You cannot write on color"): + task.write({'child_ids': [Command.create({'name': 'Foo', 'color': 1})]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + task.write({'child_ids': [Command.update(self.task_no_collabo.id, {'name': 'Foo'})]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + task.write({'child_ids': [Command.delete(self.task_no_collabo.id)]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + task.write({'child_ids': [Command.unlink(self.task_no_collabo.id)]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + task.write({'child_ids': [Command.link(self.task_no_collabo.id)]}) + with self.assertRaisesRegex(AccessError, "top-secret records"): + task.write({'child_ids': [Command.set([self.task_no_collabo.id])]}) + + # Create/update a tag through tag_ids + with self.assertRaisesRegex(AccessError, "not allowed to create 'Project Tags'"): + task.write({'tag_ids': [Command.create({'name': 'Bar'})]}) + with self.assertRaisesRegex(AccessError, "not allowed to modify 'Project Tags'"): + task.write({'tag_ids': [Command.update(self.task_tag.id, {'name': 'Bar'})]}) + with self.assertRaisesRegex(AccessError, "not allowed to delete 'Project Tags'"): + task.write({'tag_ids': [Command.delete(self.task_tag.id)]}) + + task.write({'tag_ids': [Command.link(self.task_tag.id)]}) + self.assertEqual(task.tag_ids, self.task_tag) + + task.write({'tag_ids': [Command.unlink(self.task_tag.id)]}) + self.assertFalse(task.tag_ids) + + task.write({'tag_ids': [Command.link(self.task_tag.id)]}) + task.write({'tag_ids': [Command.clear()]}) + self.assertFalse(task.tag_ids, []) + + task.write({'tag_ids': [Command.set([self.task_tag.id])]}) + self.assertEqual(task.tag_ids, self.task_tag) + + def test_portal_user_cannot_see_all_assignees(self): """ Test when the portal sees a task he cannot see all the assignees.