[FIX] mail: restrict access to mail.mail model

From now on mail.mail is considered as a technical model. Indeed people should
not really manually craft mails by hand. Instead various functional flows
should either send mails, either craft mails based on some user input.

We therefore make mail restricted to admin users. Flows creating mail.mail
are updated to use sudo, and ensure it was done in a context that makes
sense to delegate this power to the user.

Task ID 1853147
PR #32243
This commit is contained in:
Thibault Delavallée
2019-11-29 13:35:14 +00:00
parent 1838191eec
commit 69ccabb212
23 changed files with 50 additions and 39 deletions
+1 -1
View File
@@ -213,7 +213,7 @@ class Attendee(models.Model):
mail_ids.append(invitation_template.send_mail(attendee.id, email_values=email_values, notif_layout='mail.mail_notification_light'))
if force_send and mail_ids:
res = self.env['mail.mail'].browse(mail_ids).send()
res = self.env['mail.mail'].sudo().browse(mail_ids).send()
return res
+1 -1
View File
@@ -398,7 +398,7 @@ class TestCalendar(SavepointCaseWithUserDemo):
def _test_one_mail_per_attendee(self, m, partners):
# check that every attendee receive a (single) mail for the event
for partner in partners:
mail = self.env['mail.mail'].search([
mail = self.env['mail.mail'].sudo().search([
('recipient_ids', 'in', partner.id),
('subject', 'like', m.name),
])
+2 -2
View File
@@ -56,7 +56,7 @@ class TestMailSchedule(TestEventCommon):
# verify that subscription scheduler was auto-executed after each registration
self.assertEqual(len(schedulers[0].mail_registration_ids), 2, 'event: incorrect number of mail scheduled date')
mails = self.env['mail.mail'].search([('subject', 'ilike', 'registration'), ('date', '>=', now)], order='date DESC', limit=3)
mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'registration'), ('date', '>=', now)], order='date DESC', limit=3)
self.assertEqual(len(mails), 2, 'event: wrong number of registration mail sent')
for registration in schedulers[0].mail_registration_ids:
@@ -73,5 +73,5 @@ class TestMailSchedule(TestEventCommon):
self.assertTrue(schedulers[0].mail_sent, 'event: reminder scheduler should have sent an email')
self.assertTrue(schedulers[0].done, 'event: reminder scheduler should be done')
mails = self.env['mail.mail'].search([('subject', 'ilike', 'TestEventMail'), ('date', '>=', now)], order='date DESC', limit=3)
mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'TestEventMail'), ('date', '>=', now)], order='date DESC', limit=3)
self.assertEqual(len(mails), 3, 'event: wrong number of reminders in outgoing mail queue')
+2 -1
View File
@@ -607,6 +607,7 @@ class HrExpense(models.Model):
expense_template = self.env.ref(mail_template_id)
rendered_body = expense_template.render({'expense': expense}, engine='ir.qweb')
body = self.env['mail.thread']._replace_local_links(rendered_body)
# TDE TODO: seems louche, check to use notify
if expense.employee_id.user_id.partner_id:
expense.message_post(
partner_ids=expense.employee_id.user_id.partner_id.ids,
@@ -616,7 +617,7 @@ class HrExpense(models.Model):
email_layout_xmlid='mail.mail_notification_light',
)
else:
self.env['mail.mail'].create({
self.env['mail.mail'].sudo().create({
'email_from': self.env.user.email_formatted,
'author_id': self.env.user.partner_id.id,
'body_html': body,
+3 -1
View File
@@ -152,6 +152,8 @@ class LivechatController(http.Controller):
@http.route('/im_livechat/email_livechat_transcript', type='json', auth='public', cors="*")
def email_livechat_transcript(self, uuid, email):
channel = request.env['mail.channel'].sudo().search([('uuid', '=', uuid)], limit=1)
channel = request.env['mail.channel'].sudo().search([
('channel_type', '=', 'livechat'),
('uuid', '=', uuid)], limit=1)
if channel:
channel._email_livechat_transcript(email)
+1 -1
View File
@@ -171,7 +171,7 @@ class MailChannel(models.Model):
template = self.env.ref('im_livechat.livechat_email_template')
mail_body = template.render(render_context, engine='ir.qweb', minimal_qcontext=True)
mail_body = self.env['mail.thread']._replace_local_links(mail_body)
mail = self.env['mail.mail'].create({
mail = self.env['mail.mail'].sudo().create({
'subject': _('Conversation with %s') % self.livechat_operator_id.name,
'email_from': company.catchall_formatted or company.email_formatted,
'author_id': self.env.user.partner_id.id,
+1 -1
View File
@@ -278,7 +278,7 @@ class AccountMove(models.Model):
'attachment_ids': [(6, 0, self.l10n_it_einvoice_id.ids)],
})
mail_fattura = self.env['mail.mail'].with_context(wo_return_path=True).create({
mail_fattura = self.env['mail.mail'].sudo().with_context(wo_return_path=True).create({
'mail_message_id': message.id,
'email_to': self.env.company.l10n_it_address_recipient_fatturapa,
})
+2 -2
View File
@@ -367,7 +367,7 @@ class Channel(models.Model):
# Notifies the message author when his message is pending moderation if required on channel.
# The fields "email_from" and "reply_to" are filled in automatically by method create in model mail.message.
if self.moderation_notify and self.moderation_notify_msg and message_type == 'email' and moderation_status == 'pending_moderation':
self.env['mail.mail'].create({
self.env['mail.mail'].sudo().create({
'author_id': self.env.user.partner_id.id,
'email_from': self.env.user.company_id.catchall_formatted or self.env.user.company_id.email_formatted,
'body_html': self.moderation_notify_msg,
@@ -426,7 +426,7 @@ class Channel(models.Model):
'subject': _("Guidelines of channel %s") % self.name,
'recipient_ids': [(4, partner.id)]
}
mail = self.env['mail.mail'].create(create_values)
mail = self.env['mail.mail'].sudo().create(create_values)
return True
def _update_moderation_email(self, emails, status):
+1 -1
View File
@@ -134,7 +134,7 @@ class Message(models.Model):
# By setting up the inverse one2many, we avoid to have to do a search to find the mails linked to the `mail.message`
# as the cache value for this inverse one2many is up-to-date.
# Besides for new messages, and messages never sending emails, there was no mail, and it was searching for nothing.
mail_ids = fields.One2many('mail.mail', 'mail_message_id', string='Mails')
mail_ids = fields.One2many('mail.mail', 'mail_message_id', string='Mails', groups="base.group_system")
canned_response_ids = fields.One2many('mail.shortcode', 'message_ids', string="Canned Responses", store=False)
def _get_needaction(self):
+10 -2
View File
@@ -405,6 +405,11 @@ class MailTemplate(models.Model):
# EMAIL
# ----------------------------------------
def _send_check_access(self, res_ids):
records = self.env[self.model].browse(res_ids)
records.check_access_rights('read')
records.check_access_rule('read')
def send_mail(self, res_id, force_send=False, raise_exception=False, email_values=None, notif_layout=False):
""" Generates a new mail.mail. Template is rendered on record given by
res_id and model coming from template.
@@ -417,8 +422,11 @@ class MailTemplate(models.Model):
:param str notif_layout: optional notification layout to encapsulate the
generated email;
:returns: id of the mail.mail that was created """
# Grant access to send_mail only if access to related document
self.ensure_one()
Mail = self.env['mail.mail']
self._send_check_access([res_id])
Attachment = self.env['ir.attachment'] # TDE FIXME: should remove default_type from context
# create a mail_mail based on values, without attachments
@@ -447,7 +455,7 @@ class MailTemplate(models.Model):
}
body = template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
values['body_html'] = self.env['mail.thread']._replace_local_links(body)
mail = Mail.create(values)
mail = self.env['mail.mail'].sudo().create(values)
# manage attachments
for attachment in attachments:
+1 -1
View File
@@ -697,7 +697,7 @@ class MailThread(models.AbstractModel):
else:
bounce_mail_values['email_from'] = tools.decode_message_header(message, 'To')
bounce_mail_values.update(mail_values)
self.env['mail.mail'].create(bounce_mail_values).send()
self.env['mail.mail'].sudo().create(bounce_mail_values).send()
@api.model
def _routing_handle_bounce(self, email_message, message_dict):
+1 -2
View File
@@ -4,7 +4,7 @@ access_mail_message_portal,mail.message.portal,model_mail_message,base.group_por
access_mail_message_user,mail.message.user,model_mail_message,base.group_user,1,1,1,1
access_mail_mail_all,mail.mail.all,model_mail_mail,,0,0,0,0
access_mail_mail_portal,mail.mail.portal,model_mail_mail,base.group_portal,0,0,0,0
access_mail_mail_user,mail.mail.user,model_mail_mail,base.group_user,1,1,1,0
access_mail_mail_user,mail.mail.user,model_mail_mail,base.group_user,0,0,0,0
access_mail_mail_system,mail.mail.system,model_mail_mail,base.group_system,1,1,1,1
access_mail_followers_all,mail.followers.all,model_mail_followers,,1,0,0,0
access_mail_followers_portal,mail.followers.portal,model_mail_followers,base.group_portal,1,1,1,0
@@ -28,7 +28,6 @@ access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value
access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,0,0,0,0
access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,0,0,0,0
access_mail_tracking_value_system,mail.tracking.value.system,model_mail_tracking_value,base.group_system,1,1,1,1
access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1
access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1
access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0
access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
4 access_mail_message_user mail.message.user model_mail_message base.group_user 1 1 1 1
5 access_mail_mail_all mail.mail.all model_mail_mail 0 0 0 0
6 access_mail_mail_portal mail.mail.portal model_mail_mail base.group_portal 0 0 0 0
7 access_mail_mail_user mail.mail.user model_mail_mail base.group_user 1 0 1 0 1 0 0
8 access_mail_mail_system mail.mail.system model_mail_mail base.group_system 1 1 1 1
9 access_mail_followers_all mail.followers.all model_mail_followers 1 0 0 0
10 access_mail_followers_portal mail.followers.portal model_mail_followers base.group_portal 1 1 1 0
28 access_mail_tracking_value_portal mail.tracking.value.portal model_mail_tracking_value base.group_portal 0 0 0 0
29 access_mail_tracking_value_user mail.tracking.value.user model_mail_tracking_value base.group_user 0 0 0 0
30 access_mail_tracking_value_system mail.tracking.value.system model_mail_tracking_value base.group_system 1 1 1 1
access_mail_thread_all mail.thread.all model_mail_thread 1 1 1 1
31 access_publisher_warranty_contract_all publisher.warranty.contract.all model_publisher_warranty_contract 1 1 1 1
32 access_mail_template mail.template model_mail_template base.group_user 1 1 1 0
33 access_mail_template_system mail.template_system model_mail_template base.group_system 1 1 1 1
+7 -4
View File
@@ -208,7 +208,6 @@ class MailComposer(models.TransientModel):
# Mass Mailing
mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post')
Mail = self.env['mail.mail']
ActiveModel = self.env[wizard.model] if wizard.model and hasattr(self.env[wizard.model], 'message_post') else self.env['mail.thread']
if wizard.composition_mode == 'mass_post':
# do not send emails directly but use the queue instead
@@ -233,11 +232,15 @@ class MailComposer(models.TransientModel):
subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')
for res_ids in sliced_res_ids:
batch_mails = Mail
# mass mail mode: mail are sudo-ed, as when going through get_mail_values
# standard access rights on related records will be checked when browsing them
# to compute mail values. If people have access to the records they have rights
# to create lots of emails in sudo as it is consdiered as a technical model.
batch_mails_sudo = self.env['mail.mail'].sudo()
all_mail_values = wizard.get_mail_values(res_ids)
for res_id, mail_values in all_mail_values.items():
if wizard.composition_mode == 'mass_mail':
batch_mails |= Mail.create(mail_values)
batch_mails_sudo |= self.env['mail.mail'].sudo().create(mail_values)
else:
post_params = dict(
message_type=wizard.message_type,
@@ -258,7 +261,7 @@ class MailComposer(models.TransientModel):
ActiveModel.browse(res_id).message_post(**post_params)
if wizard.composition_mode == 'mass_mail':
batch_mails.send(auto_commit=auto_commit)
batch_mails_sudo.send(auto_commit=auto_commit)
def get_mail_values(self, res_ids):
"""Generate the values that will be used by send_mail to create mail_messages
+1 -2
View File
@@ -23,8 +23,7 @@ class MailMail(models.Model):
mails = super(MailMail, self).create(values_list)
for mail, values in zip(mails, values_list):
if values.get('mailing_trace_ids'):
mail_sudo = mail.sudo()
mail_sudo.mailing_trace_ids.write({'message_id': mail_sudo.message_id, 'state': 'outgoing'})
mail.mailing_trace_ids.write({'message_id': mail.message_id, 'state': 'outgoing'})
return mails
def _get_tracking_url(self):
@@ -68,7 +68,7 @@ Email: <a id="url4" href="mailto:test@odoo.com">test@odoo.com</h1>
mass_mailing.action_put_in_queue()
mass_mailing._process_mass_mailing_queue()
sent_mails = self.env['mail.mail'].search([('mailing_id', '=', mass_mailing.id)])
sent_mails = self.env['mail.mail'].sudo().search([('mailing_id', '=', mass_mailing.id)])
sent_messages = sent_mails.mapped('mail_message_id')
self.assertEqual(len(mailing_list_A.contact_ids), len(sent_messages),
@@ -14,7 +14,7 @@ class TestMassMailing(models.TransientModel):
def send_mail_test(self):
self.ensure_one()
mails = self.env['mail.mail']
mails_sudo = self.env['mail.mail'].sudo()
mailing = self.mass_mailing_id
test_emails = tools.email_split(self.email_to)
mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout')
@@ -33,7 +33,7 @@ class TestMassMailing(models.TransientModel):
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
'auto_delete': True,
}
mail = self.env['mail.mail'].create(mail_values)
mails |= mail
mails.send()
mail = self.env['mail.mail'].sudo().create(mail_values)
mails_sudo |= mail
mails_sudo.send()
return True
@@ -133,7 +133,7 @@ class TestCertificationFlow(common.TestSurveyCommon, HttpCase):
self.assertNotIn("I think they're great!", user_inputs.mapped('user_input_line_ids.value_free_text'))
self.assertIn("Just kidding, I don't like it...", user_inputs.mapped('user_input_line_ids.value_free_text'))
certification_email = self.env['mail.mail'].search([], limit=1, order="create_date desc")
certification_email = self.env['mail.mail'].sudo().search([], limit=1, order="create_date desc")
# Check certification email correctly sent and contains document
self.assertIn("User Certification for SO lines", certification_email.subject)
self.assertIn("employee@example.com", certification_email.email_to)
+1 -1
View File
@@ -12,7 +12,7 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
self.action.write({'state': 'email', 'template_id': email_template.id})
self.action.with_context(self.context).run()
# check an email is waiting for sending
mail = self.env['mail.mail'].search([('subject', '=', 'About TestingPartner')])
mail = self.env['mail.mail'].sudo().search([('subject', '=', 'About TestingPartner')])
self.assertEqual(len(mail), 1)
# check email content
self.assertEqual(mail.body, '<p>Hello TestingPartner</p>')
+3 -4
View File
@@ -15,8 +15,7 @@ class TestMailMail(TestMailCommon):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_message_notify_from_mail_mail(self):
# Due ot post-commit hooks, store send emails in every step
# self.email_to_list = []
mail = self.env['mail.mail'].create({
mail = self.env['mail.mail'].sudo().create({
'body_html': '<p>Test</p>',
'email_to': 'test@example.com',
'partner_ids': [(4, self.user_employee.partner_id.id)]
@@ -28,7 +27,7 @@ class TestMailMail(TestMailCommon):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_message_values_unicode(self):
mail = self.env['mail.mail'].create({
mail = self.env['mail.mail'].sudo().create({
'body_html': '<p>Test</p>',
'email_to': 'test.😊@example.com',
'partner_ids': [(4, self.user_employee.partner_id.id)]
@@ -45,7 +44,7 @@ class TestMailMailRace(common.TransactionCase):
'name': 'Ernest Partner',
})
# we need to simulate a mail sent by the cron task, first create mail, message and notification by hand
mail = self.env['mail.mail'].create({
mail = self.env['mail.mail'].sudo().create({
'body_html': '<p>Test</p>',
'notification': True,
'state': 'outgoing',
+1 -1
View File
@@ -48,7 +48,7 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_template_send_email(self):
mail_id = self.email_template.send_mail(self.test_record.id)
mail = self.env['mail.mail'].browse(mail_id)
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(mail.subject, 'About %s' % self.test_record.name)
self.assertEqual(mail.email_to, self.email_template.email_to)
self.assertEqual(mail.email_cc, self.email_template.email_cc)
@@ -72,7 +72,7 @@ class TestComposer(TestMailCommon, TestRecipients):
}).send_mail()
# check mail_mail
mails = self.env['mail.mail'].search([('subject', 'ilike', 'Testing')])
mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'Testing')])
for mail in mails:
self.assertEqual(mail.recipient_ids, self.partner_1 | self.partner_2,
'compose wizard: mail_mail mass mailing: mail.mail in mass mail incorrect recipients')
+2 -2
View File
@@ -86,7 +86,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
self.assertEqual(msg.channel_ids, self.env['mail.channel'])
# notifications emails should have been deleted
self.assertFalse(self.env['mail.mail'].search([('mail_message_id', '=', msg.id)]),
self.assertFalse(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)]),
'message_post: mail.mail notifications should have been auto-deleted')
@mute_logger('odoo.addons.mail.models.mail_mail')
@@ -101,7 +101,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
)
# notifications emails should not have been deleted: one for customers, one for user
self.assertEqual(len(self.env['mail.mail'].search([('mail_message_id', '=', msg.id)])), 2)
self.assertEqual(len(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)])), 2)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_post_notifications_emails_tweak(self):
+2 -2
View File
@@ -459,8 +459,8 @@ class TestMailComplexPerformance(BaseMailPerformance):
'recipient_ids': [(4, pid) for pid in self.partners.ids],
})
mail_ids = mail.ids
with self.assertQueryCount(__system__=8, emp=9):
self.env['mail.mail'].browse(mail_ids).send()
with self.assertQueryCount(__system__=8, emp=8):
self.env['mail.mail'].sudo().browse(mail_ids).send()
self.assertEqual(mail.body_html, '<p>Test</p>')
self.assertEqual(mail.reply_to, formataddr(('%s %s' % (self.env.company.name, self.umbrella.name), 'test-alias@example.com')))