Files
odoo_source/addons/account/tests/test_portal_attachment.py
T
Thibault Delavallée 17bf3d9134 [FIX] portal, account: explicitly check access on record for chatter_post
Currently using the ``chatter_post`` route to post a message implicitly checks
for access rights on related record. It is done manually in ``message_post``
that is called by the route. It is done because record name is accessed as
sudo and an explicit access has been added because of fear of information
leak.

However this is a bit out of scope for ``message_post`` to perform that check
at that point. As the underlying record is accessed numerous times, adding
an explicit check just adds queries.

In portal, users may have a token and/or a hash and a partner_id, in which
case they enter sudo mode if valid, and an error is raised otherwise. However
when not using a token and/or a hash and a PID access check is now done
directly in the controller helper. Moreover this allows to have the access
error directly instead of being possibly hidden by another error, which was
the case before this fix (was actually raising about missing email_from).

In this commit we add an explicit check on record access before entering
the post computation. That way access errors are directly raised.

A fix in accounting is necessary to ensure the right error is raised. Indeed
otherwise it currently raises due to non existing email_from, and should
raise due to ACL instead. Tests are now more inlined with classic flows.

Task-2710804 (Mail: Clean MailThread API)

Part-of: odoo/odoo#99654
2022-09-09 11:12:29 +02:00

288 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
from odoo.tests.common import tagged
import json
from odoo import http
from odoo.tools import mute_logger
@tagged('post_install', '-at_install')
class TestPortalAttachment(AccountTestInvoicingHttpCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.out_invoice = cls.env['account.move'].with_context(tracking_disable=True).create({
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'invoice_date': '2019-05-01',
'date': '2019-05-01',
'invoice_line_ids': [
(0, 0, {'name': 'line1', 'price_unit': 100.0}),
],
})
cls.invoice_base_url = cls.out_invoice.get_base_url()
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_01_portal_attachment(self):
"""Test the portal chatter attachment route."""
self.partner_a.write({ # ensure an email for message_post
'email': 'partner.a@test.example.com',
})
self.authenticate(None, None)
# Test public user can't create attachment without token of document
res = self.url_open(
url=f'{self.invoice_base_url}/portal/attachment/add',
data={
'name': "new attachment",
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'csrf_token': http.Request.csrf_token(self),
},
files=[('file', ('test.txt', b'test', 'plain/text'))],
)
self.assertEqual(res.status_code, 400)
self.assertIn("you do not have the rights", res.text)
# Test public user can create attachment with token
res = self.url_open(
url=f'{self.invoice_base_url}/portal/attachment/add',
data={
'name': "new attachment",
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'csrf_token': http.Request.csrf_token(self),
'access_token': self.out_invoice._portal_ensure_token(),
},
files=[('file', ('test.txt', b'test', 'plain/text'))],
)
self.assertEqual(res.status_code, 200)
create_res = json.loads(res.content.decode('utf-8'))
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
# Test created attachment is private
res_binary = self.url_open('/web/content/%d' % create_res['id'])
self.assertEqual(res_binary.status_code, 404)
# Test created access_token is working
res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
self.assertEqual(res_binary.status_code, 200)
# Test mimetype is neutered as non-admin
res = self.url_open(
url=f'{self.invoice_base_url}/portal/attachment/add',
data={
'name': "new attachment",
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'csrf_token': http.Request.csrf_token(self),
'access_token': self.out_invoice._portal_ensure_token(),
},
files=[('file', ('test.svg', b'<svg></svg>', 'image/svg+xml'))],
)
self.assertEqual(res.status_code, 200)
create_res = json.loads(res.content.decode('utf-8'))
self.assertEqual(create_res['mimetype'], 'text/plain')
res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
self.assertEqual(res_binary.headers['Content-Type'], 'text/plain; charset=utf-8')
self.assertEqual(res_binary.content, b'<svg></svg>')
res_image = self.url_open('/web/image/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
self.assertEqual(res_image.headers['Content-Type'], 'text/plain; charset=utf-8')
self.assertEqual(res_image.content, b'<svg></svg>')
# Test attachment can't be removed without valid token
res = self.opener.post(
url=f'{self.invoice_base_url}/portal/attachment/remove',
json={
'params': {
'attachment_id': create_res['id'],
'access_token': "wrong",
},
},
)
self.assertEqual(res.status_code, 200)
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
self.assertIn("you do not have the rights", res.text)
# Test attachment can be removed with token if "pending" state
res = self.opener.post(
url=f'{self.invoice_base_url}/portal/attachment/remove',
json={
'params': {
'attachment_id': create_res['id'],
'access_token': create_res['access_token'],
},
},
)
self.assertEqual(res.status_code, 200)
remove_res = json.loads(res.content.decode('utf-8'))['result']
self.assertFalse(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
self.assertTrue(remove_res is True)
# Test attachment can't be removed if not "pending" state
attachment = self.env['ir.attachment'].create({
'name': 'an attachment',
'access_token': self.env['ir.attachment']._generate_access_token(),
})
res = self.opener.post(
url=f'{self.invoice_base_url}/portal/attachment/remove',
json={
'params': {
'attachment_id': attachment.id,
'access_token': attachment.access_token,
},
},
)
self.assertEqual(res.status_code, 200)
self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', attachment.id)]))
self.assertIn("not in a pending state", res.text)
# Test attachment can't be removed if attached to a message
attachment.write({
'res_model': 'mail.compose.message',
'res_id': 0,
})
attachment.flush_recordset()
message = self.env['mail.message'].create({
'attachment_ids': [(6, 0, attachment.ids)],
})
res = self.opener.post(
url=f'{self.invoice_base_url}/portal/attachment/remove',
json={
'params': {
'attachment_id': attachment.id,
'access_token': attachment.access_token,
},
},
)
self.assertEqual(res.status_code, 200)
self.assertTrue(attachment.exists())
self.assertIn("it is linked to a message", res.text)
message.sudo().unlink()
# Test attachment can't be associated if no attachment token.
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': ['false'],
'csrf_token': http.Request.csrf_token(self),
},
},
)
self.assertEqual(res.status_code, 200)
self.assertIn("The attachment %s does not exist or you do not have the rights to access it." % attachment.id, res.text)
# Test attachment can't be associated if no main document token
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.Request.csrf_token(self),
},
},
)
self.assertEqual(res.status_code, 200)
self.assertIn("You are not allowed to access 'Journal Entry' (account.move) records.", res.text)
# Test attachment can't be associated if not "pending" state
self.assertFalse(self.out_invoice.message_ids)
attachment.write({'res_model': 'model'})
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.Request.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
self.out_invoice.invalidate_recordset(['message_ids'])
self.assertEqual(len(self.out_invoice.message_ids), 1)
self.assertEqual(self.out_invoice.message_ids.body, "<p>test message 1</p>")
self.assertFalse(self.out_invoice.message_ids.attachment_ids)
# Test attachment can't be associated if not correct user
attachment.write({'res_model': 'mail.compose.message'})
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 2",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.Request.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
self.out_invoice.invalidate_recordset(['message_ids'])
self.assertEqual(len(self.out_invoice.message_ids), 2)
self.assertEqual(self.out_invoice.message_ids[0].author_id, self.partner_a)
self.assertEqual(self.out_invoice.message_ids[0].body, "<p>test message 2</p>")
self.assertEqual(self.out_invoice.message_ids[0].email_from, self.partner_a.email_formatted)
self.assertFalse(self.out_invoice.message_ids.attachment_ids)
# Test attachment can be associated if all good (complete flow)
res = self.url_open(
url=f'{self.invoice_base_url}/portal/attachment/add',
data={
'name': "final attachment",
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'csrf_token': http.Request.csrf_token(self),
'access_token': self.out_invoice._portal_ensure_token(),
},
files=[('file', ('test.txt', b'test', 'plain/text'))],
)
self.assertEqual(res.status_code, 200)
create_res = json.loads(res.content.decode('utf-8'))
self.assertEqual(create_res['name'], "final attachment")
res = self.opener.post(
url=f'{self.invoice_base_url}/mail/chatter_post',
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 3",
'attachment_ids': [create_res['id']],
'attachment_tokens': [create_res['access_token']],
'csrf_token': http.Request.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
self.out_invoice.invalidate_recordset(['message_ids'])
self.assertEqual(len(self.out_invoice.message_ids), 3)
self.assertEqual(self.out_invoice.message_ids[0].body, "<p>test message 3</p>")
self.assertEqual(len(self.out_invoice.message_ids[0].attachment_ids), 1)