[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:
Denis Ledoux
2024-02-07 11:05:11 +00:00
parent 9b40507c01
commit 4ae9104d00
2 changed files with 119 additions and 10 deletions
+27 -8
View File
@@ -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()
+92 -2
View File
@@ -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.