[FIX] web_editor, *: allow internal user to upload unsplash image

* web_unsplash, website_forum

With [1], the access errors when a portal user were using the media
dialog got fixed. It also came with the unsplash capability for portal
user in the media dialog.

Still, there was a remaining problematic point: an internal user can't
upload an unsplash image in the backend through the media dialog. It
doesn't really make sense for an internal user to have less right than
the portal user.

This commit corrects that part.

Note that only unsplash images were problematic, not regular uploaded
image as the difference was that unsplash images attachment are saved
with an `url` property, which was triggering due to [2].

Finally, it was chosen to make that "bypass" more robust and opt in, so
forum and unsplash are allowing their use cases to go through, it's not
done in a generic way anymore.

Also fixing a small issue about the res_id not being sent when editing a
forum post, because it was assuming that the URL would end with the ID
which is not the case when you edit your answer.

[1]: https://github.com/odoo/odoo/commit/e10493711879c7f0cc8832db3f1936c622ea605c
[2]: https://github.com/odoo/odoo/commit/bfffe39f1376a56226572295b945a2cc73ba50ce

task-3007844

closes odoo/odoo#103138

Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Romain Derie
2022-10-12 10:34:58 +02:00
committed by qsm-odoo
parent 5a7fd7fe96
commit ab2adbdc72
9 changed files with 78 additions and 19 deletions
+10 -10
View File
@@ -292,6 +292,8 @@ class Web_Editor(http.Controller):
def _attachment_create(self, name='', data=False, url=False, res_id=False, res_model='ir.ui.view'):
"""Create and return a new attachment."""
IrAttachment = request.env['ir.attachment']
if name.lower().endswith('.bmp'):
# Avoid mismatch between content type and mimetype, see commit msg
name = name[:-4]
@@ -323,15 +325,13 @@ class Web_Editor(http.Controller):
else:
raise UserError(_("You need to specify either data or url to create an attachment."))
# If the user is a portal and has access rights on the res_model (and
# on the res_id if it exists), we can create the attachment. Otherwise,
# one of the check will raise an error and the attachment will not be
# created
if request.env.user.has_group('base.group_portal'):
request.env[res_model].check_access_rights('write')
if res_id:
request.env[res_model].browse(res_id).check_access_rule('write')
attachment = request.env['ir.attachment'].sudo().create(attachment_data)
# Despite the user having no right to create an attachment, he can still
# create an image attachment through some flows
if (
not request.env.is_admin()
and IrAttachment._can_bypass_rights_on_media_dialog(**attachment_data)
):
attachment = IrAttachment.sudo().create(attachment_data)
# When portal users upload an attachment with the wysiwyg widget,
# the access token is needed to use the image in the editor. If
# the attachment is not public, the user won't be able to generate
@@ -339,7 +339,7 @@ class Web_Editor(http.Controller):
if not attachment_data['public']:
attachment.sudo().generate_access_token()
else:
attachment = request.env['ir.attachment'].create(attachment_data)
attachment = IrAttachment.create(attachment_data)
return attachment
@@ -63,3 +63,12 @@ class IrAttachment(models.Model):
"""Return a dict with the values that we need on the media dialog."""
self.ensure_one()
return self._read_format(['id', 'name', 'description', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height', 'original_id'])[0]
def _can_bypass_rights_on_media_dialog(self, **attachment_data):
""" This method is meant to be overridden, for instance to allow to
create image attachment despite the user not allowed to create
attachment, eg:
- Portal user uploading an image on the forum (bypass acl)
- Non admin user uploading an unsplash image (bypass binary/url check)
"""
return False
+2 -1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_attachment
from . import ir_qweb_fields
from . import res_config_settings
from . import res_users
from . import ir_qweb_fields
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class Attachment(models.Model):
_inherit = "ir.attachment"
def _can_bypass_rights_on_media_dialog(self, **attachment_data):
# We need to allow and sudo the case of an "url + file" attachment,
# which is by default forbidden for non admin.
# See `_check_serving_attachments`
forbidden = 'url' in attachment_data and attachment_data.get('type', 'binary') == 'binary'
if forbidden and attachment_data['url'].startswith('/unsplash/'):
return True
return super()._can_bypass_rights_on_media_dialog(**attachment_data)
+1
View File
@@ -2,5 +2,6 @@
from . import forum
from . import gamification
from . import ir_attachment
from . import res_users
from . import website
+4
View File
@@ -374,6 +374,9 @@ class Post(models.Model):
can_post = fields.Boolean('Can Automatically be Validated', compute='_get_post_karma_rights', compute_sudo=False)
can_flag = fields.Boolean('Can Flag', compute='_get_post_karma_rights', compute_sudo=False)
can_moderate = fields.Boolean('Can Moderate', compute='_get_post_karma_rights', compute_sudo=False)
can_use_full_editor = fields.Boolean(
compute='_get_post_karma_rights', compute_sudo=False,
help="Editor Features: image and links")
def _search_can_view(self, operator, value):
if operator not in ('=', '!=', '<>'):
@@ -495,6 +498,7 @@ class Post(models.Model):
post.can_post = is_admin or user.karma >= post.forum_id.karma_post
post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag
post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate
post.can_use_full_editor = is_admin or user.karma >= post.forum_id.karma_editor
def _update_content(self, content, forum_id):
forum = self.env['forum.forum'].browse(forum_id)
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class Attachment(models.Model):
_inherit = "ir.attachment"
def _can_bypass_rights_on_media_dialog(self, **attachment_data):
# Bypass the attachment create ACL and let the user create the image
# attachment if he has write right on the model (the image attachment
# will be bound to this model's record).
res_model = attachment_data['res_model']
res_id = attachment_data.get('res_id')
if (
res_model == 'forum.post' and res_id
and self.env['forum.post'].browse(res_id).can_use_full_editor
):
return True
return super()._can_bypass_rights_on_media_dialog(**attachment_data)
@@ -129,7 +129,11 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
recordInfo: {
context: self._getContext(),
res_model: 'forum.post',
res_id: +window.location.pathname.split('-').pop(),
// Id is retrieved from URL, which is either:
// - /forum/name-1/post/something-5
// - /forum/name-1/post/something-5/edit
// TODO: Make this more robust.
res_id: +window.location.pathname.split('-').slice(-1)[0].split('/')[0],
},
resizable: true,
userGeneratedContent: true,
@@ -10,10 +10,8 @@ class TestController(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
portal_user = new_test_user(cls.env, login='portal_user', groups='base.group_portal')
cls.portal = portal_user.login
admin_user = new_test_user(cls.env, login='admin_user', groups='base.group_user,base.group_system')
cls.admin = admin_user.login
cls.portal_user = new_test_user(cls.env, login='portal_user', groups='base.group_portal')
cls.admin_user = new_test_user(cls.env, login='admin_user', groups='base.group_user,base.group_system')
cls.headers = {"Content-Type": "application/json"}
cls.pixel = 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs='
@@ -29,15 +27,16 @@ class TestController(HttpCase):
}
def test_01_portal_attachment(self):
self.authenticate(self.portal, self.portal)
payload = self._build_payload({"name": "pixel", "data": self.pixel, "is_image": True, "res_model": "forum.post"})
self.authenticate(self.portal_user.login, self.portal_user.login)
payload = self._build_payload({'name': 'pixel', 'data': self.pixel, 'is_image': True, 'res_model': 'forum.post', 'res_id': 1})
self.portal_user.karma = 30
response = self.url_open('/web_editor/attachment/add_data', data=json.dumps(payload), headers=self.headers, timeout=60000)
self.assertEqual(200, response.status_code)
attachment = self.env['ir.attachment'].search([('name', '=', 'pixel')])
self.assertTrue(attachment)
def test_02_admin_attachment(self):
self.authenticate(self.admin, self.admin)
self.authenticate(self.admin_user.login, self.admin_user.login)
payload = self._build_payload({"name": "pixel", "data": self.pixel, "is_image": True, "res_model": "forum.post"})
response = self.url_open('/web_editor/attachment/add_data', data=json.dumps(payload), headers=self.headers)
self.assertEqual(200, response.status_code)