[FIX] project: sharing, restrict child_ids an tag_ids for portals
Restrict collaborator portals to: - Change unallowed fields on subtasks - Create/Update/Delete tags. They can only link, unlink tags to tasks. task-3698146 closes odoo/odoo#152992 X-original-commit: 32c21d651c32c978f3cf67105e1c2eca9de2334d Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com> Signed-off-by: Denis Ledoux (dle) <dle@odoo.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user