[IMP] mail, im_livechat, *: simplify channel ACL

* = bus, crm_livechat, hr, mail_bot, test_discuss_full, test_mail,
    website_livechat

Now that livechat uses guest, we can write proper ACL for channel and
channel member to check if the current user/guest is a member.

This allows removing most sudo in code and to simplify search domains.
Remaining sudo in discuss folder have been reviewed and commented.

task-3394829

closes odoo/odoo#138330

Related: odoo/upgrade#5295
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Sébastien Theys
2023-10-24 12:38:21 +00:00
parent 1dd012608a
commit 005b76462a
56 changed files with 1128 additions and 834 deletions
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.crm_livechat.tests import chatbot_common
from odoo.tests.common import users
from odoo.tests.common import tagged, users
@tagged("post_install", "-at_install")
class CrmChatbotCase(chatbot_common.CrmChatbotCase):
@users('user_public')
+12 -6
View File
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.addons.crm.tests.common import TestCrmCommon
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.tests.common import users, HttpCase
from odoo.tests.common import HttpCase, tagged, users
@tagged("post_install", "-at_install")
class TestLivechatLead(HttpCase, TestCrmCommon):
@classmethod
@@ -59,17 +59,23 @@ class TestLivechatLead(HttpCase, TestCrmCommon):
self.assertTrue(any(bool(m.guest_id) for m in channel.channel_member_ids))
# public + someone else: no customer (as they were anonymous)
channel.write({
# sudo: discuss.channel.member - removing non-self member for test setup purposes
channel.sudo().write({
'channel_partner_ids': [(4, self.user_sales_manager.partner_id.id)]
})
lead = channel._convert_visitor_to_lead(self.env.user.partner_id, '/lead TestLead command')
self.assertEqual(lead.partner_id, self.env['res.partner'])
@users('user_sales_leads')
def test_crm_lead_creation_portal(self):
# portal: should be set as customer
channel = self.env['discuss.channel'].create({
'name': 'Chat with Visitor',
'channel_partner_ids': [(4, self.user_portal.partner_id.id)]
self.authenticate("user_portal", "user_portal")
channel_info = self.make_jsonrpc_request("/im_livechat/get_session", {
'anonymous_name': 'Visitor',
'channel_id': self.livechat_channel.id,
'persisted': True,
})
channel = self.env['discuss.channel'].browse(channel_info['id'])
lead = channel._convert_visitor_to_lead(self.env.user.partner_id, '/lead TestLead command')
self.assertEqual(
+2 -1
View File
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.tests.common import tagged
@tagged("post_install", "-at_install")
class TestChannel(TestHrCommon):
@classmethod
+5 -1
View File
@@ -1,5 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.exceptions import NotFound
from odoo import _
from odoo.http import route, request
from odoo.addons.mail.controllers.attachment import AttachmentController
@@ -11,7 +13,9 @@ class LivechatAttachmentController(AttachmentController):
@route()
@add_guest_to_context
def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs):
thread = request.env[thread_model]._get_from_context_or_raise(int(thread_id))
thread = request.env[thread_model].search([("id", "=", thread_id)])
if not thread:
raise NotFound()
if (
thread_model == "discuss.channel"
and thread.channel_type == "livechat"
+24 -16
View File
@@ -6,7 +6,9 @@ from werkzeug.exceptions import NotFound
from urllib.parse import urlsplit
from odoo import http, tools, _, release
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import replace_exceptions
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
@@ -192,12 +194,15 @@ class LivechatController(http.Controller):
'chatbot_script_id': chatbot_script.id if chatbot_script else None
}
channel = request.env['discuss.channel'].with_context(mail_create_nosubscribe=False).sudo().create(channel_vals)
__, guest = channel._find_or_create_persona_for_channel(
guest_name=self._get_guest_name(),
country_code=request.geoip.country_code,
timezone=request.env['mail.guest']._get_timezone_from_request(request),
post_joined_message=False
)
with replace_exceptions(UserError, by=NotFound()):
# sudo: mail.guest - creating a guest and their member in a dedicated channel created from livechat
__, guest = channel.sudo()._find_or_create_persona_for_channel(
guest_name=self._get_guest_name(),
country_code=request.geoip.country_code,
timezone=request.env['mail.guest']._get_timezone_from_request(request),
post_joined_message=False
)
channel = channel.with_context(guest=guest) # a new guest was possibly created
if not chatbot_script or chatbot_script.operator_partner_id != channel.livechat_operator_id:
channel._broadcast([channel.livechat_operator_id.id])
channel_info = channel._channel_info()[0]
@@ -264,16 +269,19 @@ class LivechatController(http.Controller):
if channel:
channel._email_livechat_transcript(email)
@http.route('/im_livechat/visitor_leave_session', type='json', auth="public")
@http.route("/im_livechat/visitor_leave_session", type="json", auth="public")
@add_guest_to_context
def visitor_leave_session(self, uuid):
""" Called when the livechat visitor leaves the conversation.
This will clean the chat request and warn the operator that the conversation is over.
This allows also to re-send a new chat request to the visitor, as while the visitor is
in conversation with an operator, it's not possible to send the visitor a chat request."""
discuss_channel = request.env['discuss.channel'].sudo().search([('uuid', '=', uuid)])
if not discuss_channel:
"""Called when the livechat visitor leaves the conversation.
This will clean the chat request and warn the operator that the conversation is over.
This allows also to re-send a new chat request to the visitor, as while the visitor is
in conversation with an operator, it's not possible to send the visitor a chat request."""
# sudo: channel access is validated with uuid
channel_sudo = request.env["discuss.channel"].sudo().search([("uuid", "=", uuid)])
if not channel_sudo:
return
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=discuss_channel.id)
channel_member_sudo._rtc_leave_call()
discuss_channel._close_livechat_session()
domain = [("channel_id", "=", channel_sudo.id), ("is_self", "=", True)]
member = request.env["discuss.channel.member"].search(domain)
# sudo: discuss.channel.rtc.session - member of current user can leave call
member.sudo()._rtc_leave_call()
channel_sudo._close_livechat_session()
+14 -1
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.tools import email_normalize, html_escape, html2plaintext, plaintext2html
from markupsafe import Markup
@@ -29,6 +30,17 @@ class DiscussChannel(models.Model):
_sql_constraints = [('livechat_operator_id', "CHECK((channel_type = 'livechat' and livechat_operator_id is not null) or (channel_type != 'livechat'))",
'Livechat Operator ID is required for a channel of type livechat.')]
def _compute_is_accessible(self):
super()._compute_is_accessible()
if self.env.user.has_group("im_livechat.im_livechat_group_manager"):
self.filtered(lambda channel: channel.channel_type == "livechat").is_accessible = True
def _search_is_accessible(self, operator, operand):
res = super()._search_is_accessible(operator, operand)
if self.env.user.has_group("im_livechat.im_livechat_group_manager"):
return expression.OR([res, [("channel_type", "=", "livechat")]])
return res
@api.depends('message_ids')
def _compute_duration(self):
for record in self:
@@ -50,7 +62,8 @@ class DiscussChannel(models.Model):
channel_infos_dict = dict((c['id'], c) for c in channel_infos)
for channel in self:
if channel.chatbot_current_step_id:
channel_infos_dict[channel.id]["chatbot_script_id"] = channel.chatbot_current_step_id.chatbot_script_id.id
# sudo: chatbot.script.step - returning the current script of the channel
channel_infos_dict[channel.id]["chatbot_script_id"] = channel.chatbot_current_step_id.sudo().chatbot_script_id.id
channel_infos_dict[channel.id]['anonymous_name'] = channel.anonymous_name
channel_infos_dict[channel.id]['anonymous_country'] = {
'code': channel.country_id.code,
+2 -1
View File
@@ -41,8 +41,9 @@ class MailMessage(models.Model):
if message_sudo.author_id.user_livechat_username:
del vals['author']['name']
vals['author']['user_livechat_username'] = message_sudo.author_id.user_livechat_username
# sudo: chatbot.script.step - checking whether the current message is from chatbot
if discuss_channel.chatbot_current_step_id \
and message_sudo.author_id == discuss_channel.chatbot_current_step_id.chatbot_script_id.operator_partner_id:
and message_sudo.author_id == discuss_channel.chatbot_current_step_id.sudo().chatbot_script_id.operator_partner_id:
chatbot_message_id = self.env['chatbot.message'].sudo().search([
('mail_message_id', '=', message_sudo.id)], limit=1)
if chatbot_message_id.script_step_id:
-11
View File
@@ -34,17 +34,6 @@ class Partners(models.Model):
})
return result
def _get_channels_as_member(self):
channels = super()._get_channels_as_member()
channels |= self.env['discuss.channel'].search([
('channel_type', '=', 'livechat'),
('channel_member_ids', 'in', self.env['discuss.channel.member'].sudo()._search([
('partner_id', '=', self.id),
('is_pinned', '=', True),
])),
])
return channels
@api.depends('user_ids.livechat_username')
def _compute_user_livechat_username(self):
for partner in self:
@@ -1,6 +1,5 @@
<?xml version="1.0"?>
<odoo>
<record id="module_category_im_livechat" model="ir.module.category">
<field name="name">Live Chat</field>
<field name="sequence" eval="20" />
@@ -21,17 +20,25 @@
</record>
<data noupdate="1">
<record id="im_livechat_rule_manager_read_all_discuss_channel" model="ir.rule">
<field name="name">Livechat: Administrator: read all livechat channel</field>
<record id="ir_rule_discuss_channel_group_im_livechat_group_manager" model="ir.rule">
<field name="name">discuss.channel: livechat manager can read all livechat channels</field>
<field name="model_id" ref="mail.model_discuss_channel"/>
<field name="groups" eval="[(4, ref('im_livechat_group_manager'))]"/>
<field name="domain_force">[('channel_type', '=', 'livechat')]</field>
<field name="perm_read" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_discuss_channel_member_group_im_livechat_group_manager" model="ir.rule">
<field name="name">discuss.channel.member: livechat manager can read all livechat channel members</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups" eval="[(4, ref('im_livechat_group_manager'))]"/>
<field name="domain_force">[('channel_id.channel_type', '=', 'livechat')]</field>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('im_livechat.im_livechat_group_manager'))]"/>
@@ -77,15 +77,11 @@ patch(MockServer.prototype, {
/**
* Simulates `_find_or_create_persona_for_channel` on `discuss.channel`.
*/
_mockDiscussChannel__findOrCreatePersonaForChannel(channelId, guestName, addAsMember = true) {
_mockDiscussChannel__findOrCreatePersonaForChannel(channelId, guestName) {
if (this._mockDiscussChannelMember__getAsSudoFromContext(channelId)) {
return;
}
const guestId = this._mockMailGuest__findOrCreateForChannel(
channelId,
guestName,
addAsMember
);
const guestId = this._mockMailGuest__findOrCreateForChannel(channelId, guestName);
this._mockMailGuest__setAuthCookie(guestId);
},
});
@@ -9,15 +9,13 @@ patch(MockServer.prototype, {
/**
* Simulates `_find_or_create_for_channel` on `mail.guest`.
*/
_mockMailGuest__findOrCreateForChannel(channelId, guestName, addAsMember = true) {
_mockMailGuest__findOrCreateForChannel(channelId, guestName) {
const guestId =
this._mockMailGuest__getGuestFromContext()?.id ??
this.pyEnv["mail.guest"].create({ name: guestName });
if (addAsMember) {
this.pyEnv["discuss.channel"].write([channelId], {
channel_member_ids: [Command.create({ guest_id: guestId })],
});
}
this.pyEnv["discuss.channel"].write([channelId], {
channel_member_ids: [Command.create({ guest_id: guestId })],
});
return guestId;
},
/**
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.im_livechat.tests import chatbot_common
from odoo.exceptions import ValidationError
from odoo.tests.common import tagged
@tagged("post_install", "-at_install")
class ChatbotCase(chatbot_common.ChatbotCase):
def test_chatbot_duplicate(self):
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from freezegun import freeze_time
from unittest.mock import patch, PropertyMock
from odoo import fields
from odoo.addons.im_livechat.tests.common import TestImLivechatCommon
from odoo.tests.common import tagged
@tagged("post_install", "-at_install")
class TestGetDiscussChannel(TestImLivechatCommon):
def test_get_discuss_channel(self):
"""For a livechat with 5 available operators, we open 5 channels 5 times (25 channels total).
@@ -139,13 +141,20 @@ class TestGetDiscussChannel(TestImLivechatCommon):
return discuss_channels
def test_channel_not_pinned_for_operator_before_first_message(self):
channel_info = self.make_jsonrpc_request('/im_livechat/get_session', {'anonymous_name': 'whatever', 'channel_id': self.livechat_channel.id})
operator_channel_member = self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', 'in', self.operators.partner_id.ids)])
self.assertEqual(len(operator_channel_member), 1, "operator should be member of channel")
self.assertFalse(operator_channel_member.is_pinned, "channel should not be pinned for operator initially")
self.env['discuss.channel'].browse(channel_info['id']).message_post(body='cc')
self.assertTrue(operator_channel_member.is_pinned, "channel should be pinned for operator after visitor sent a message")
self.assertIn(channel_info['id'], operator_channel_member.partner_id._get_channels_as_member().ids, "channel should be fetched by operator on new page")
operator = self.operators[0]
params = {
"anonymous_name": "whatever",
"channel_id": self.livechat_channel.id,
"previous_operator_id": operator.partner_id.id
}
channel_id = self.make_jsonrpc_request("/im_livechat/get_session", params)["id"]
member_domain = [("channel_id", "=", channel_id), ("is_self", "=", True)]
member = self.env["discuss.channel.member"].with_user(operator).search(member_domain)
self.assertEqual(len(member), 1, "operator should be member of channel")
self.assertFalse(member.is_pinned, "channel should not be pinned for operator initially")
self.env["discuss.channel"].browse(channel_id).message_post(body="cc")
self.assertTrue(member.is_pinned, "channel should be pinned for operator after visitor sent a message")
self.assertIn(channel_id, [c["id"] for c in operator._init_messaging()["channels"]], "channel should be fetched by operator on new page")
def test_operator_livechat_username(self):
"""Ensures the operator livechat_username is returned by `_channel_fetch_message`, which is
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.addons.im_livechat.tests.common import TestImLivechatCommon
from odoo.tests.common import tagged
@tagged("post_install", "-at_install")
class TestImLivechatReport(TestImLivechatCommon):
def setUp(self):
super().setUp()
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<search string="Search history">
<field name="name" string="Participant"/>
<filter name="filter_my_sessions" domain="[('channel_member_ids.partner_id.user_ids.id', 'in', [uid])]" string="My Sessions"/>
<filter name="filter_my_sessions" domain="[('is_member', '=', True)]" string="My Sessions"/>
<separator/>
<filter name="filter_session_date" date="create_date" string="Session Date"/>
<separator/>
+17 -13
View File
@@ -15,8 +15,8 @@ from ..models.discuss.mail_guest import add_guest_to_context
logger = logging.getLogger(__name__)
class AttachmentController(http.Controller):
class AttachmentController(http.Controller):
def _make_zip(self, name, attachments):
streams = (request.env['ir.binary']._get_stream_from(record, 'raw') for record in attachments)
# TODO: zip on-the-fly while streaming instead of loading the
@@ -47,7 +47,9 @@ class AttachmentController(http.Controller):
@http.route("/mail/attachment/upload", methods=["POST"], type="http", auth="public")
@add_guest_to_context
def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs):
thread = request.env[thread_model]._get_from_context_or_raise(int(thread_id))
thread = request.env[thread_model].search([("id", "=", thread_id)])
if not thread:
raise NotFound()
if thread_model == "discuss.channel" and not thread.allow_public_upload and not request.env.user._is_internal():
raise AccessError(_("You are not allowed to upload attachments on this channel."))
vals = {
@@ -65,11 +67,12 @@ class AttachmentController(http.Controller):
"res_model": "mail.compose.message",
}
)
if thread.env.user.share:
if request.env.user.share:
# Only generate the access token if absolutely necessary (= not for internal user).
vals["access_token"] = thread.env["ir.attachment"]._generate_access_token()
vals["access_token"] = request.env["ir.attachment"]._generate_access_token()
try:
attachment = thread.env["ir.attachment"].create(vals)
# sudo: ir.attachment - posting a new attachment on an accessible thread
attachment = request.env["ir.attachment"].sudo().create(vals)
attachment._post_add_create(**kwargs)
attachmentData = attachment._attachment_format()[0]
if attachment.access_token:
@@ -81,22 +84,23 @@ class AttachmentController(http.Controller):
@http.route("/mail/attachment/delete", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def mail_attachment_delete(self, attachment_id, access_token=None):
attachment_sudo = request.env["ir.attachment"].browse(int(attachment_id)).sudo().exists()
guest = request.env["mail.guest"]._get_guest_from_context()
message_sudo = guest.env["mail.message"].sudo().search([("attachment_ids", "in", attachment_sudo.ids)], limit=1)
if not attachment_sudo:
attachment = request.env["ir.attachment"].browse(int(attachment_id)).exists()
if not attachment:
target = request.env.user.partner_id
request.env["bus.bus"]._sendone(target, "ir.attachment/delete", {"id": attachment_id})
return
message = request.env["mail.message"].search([("attachment_ids", "in", attachment.ids)], limit=1)
if not request.env.user.share:
# Check through standard access rights/rules for internal users.
attachment_sudo.sudo(False)._delete_and_notify(message_sudo)
attachment._delete_and_notify(message)
return
# For non-internal users 2 cases are supported:
# - Either the attachment is linked to a message: verify the request is made by the author of the message (portal user or guest).
# - Either a valid access token is given: also verify the message is pending (because unfortunately in portal a token is also provided to guest for viewing others' attachments).
if message_sudo:
if not message_sudo.is_current_user_or_guest_author:
# sudo: ir.attachment: access is validated below with membership of message or access token
attachment_sudo = attachment.sudo()
if message:
if not message.is_current_user_or_guest_author:
raise NotFound()
else:
if (
@@ -107,7 +111,7 @@ class AttachmentController(http.Controller):
raise NotFound()
if attachment_sudo.res_model != "mail.compose.message" or attachment_sudo.res_id != 0:
raise NotFound()
attachment_sudo._delete_and_notify(message_sudo)
attachment_sudo._delete_and_notify(message)
@http.route(['/mail/attachment/zip'], methods=["POST"], type="http", auth="public")
def mail_attachment_get_zip(self, file_ids, zip_name, **kw):
+26 -51
View File
@@ -5,9 +5,10 @@ from werkzeug.exceptions import NotFound
from odoo import http
from odoo.http import request
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
from odoo.addons.web.controllers.binary import Binary
class BinaryController(http.Controller):
class BinaryController(Binary):
@http.route(
"/discuss/channel/<int:channel_id>/partner/<int:partner_id>/avatar_128",
methods=["GET"],
@@ -16,81 +17,52 @@ class BinaryController(http.Controller):
)
@add_guest_to_context
def discuss_channel_partner_avatar_128(self, channel_id, partner_id, **kwargs):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context(channel_id=channel_id)
partner_sudo = channel_member_sudo.env["res.partner"].browse(partner_id).exists()
placeholder = partner_sudo._avatar_get_placeholder_path()
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
partner = request.env["res.partner"].browse(partner_id).exists()
domain = [("channel_id", "=", channel_id), ("partner_id", "=", partner_id)]
if channel_member_sudo and channel_member_sudo.env["discuss.channel.member"].search(domain, limit=1):
if channel and partner and request.env["discuss.channel.member"].search(domain):
# sudo: res.partner - the partner is in the same channel as the current user, so they can see their avatar
return (
request.env["ir.binary"]
._get_image_stream_from(partner_sudo, field_name="avatar_128", placeholder=placeholder)
.get_response()
request.env["ir.binary"]._get_image_stream_from(partner.sudo(), field_name="avatar_128").get_response()
)
if request.env.user.share:
return request.env["ir.binary"]._get_placeholder_stream(placeholder).get_response()
return (
request.env["ir.binary"]
._get_image_stream_from(partner_sudo.sudo(False), field_name="avatar_128", placeholder=placeholder)
.get_response()
)
return self.content_image(model="res.partner", id=partner_id, field="avatar_128")
@http.route(
"/discuss/channel/<int:channel_id>/guest/<int:guest_id>/avatar_128", methods=["GET"], type="http", auth="public"
)
@add_guest_to_context
def discuss_channel_guest_avatar_128(self, channel_id, guest_id, **kwargs):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context(channel_id=channel_id)
guest_sudo = channel_member_sudo.env["mail.guest"].browse(guest_id).exists()
placeholder = guest_sudo._avatar_get_placeholder_path()
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
guest = request.env["mail.guest"].browse(guest_id).exists()
domain = [("channel_id", "=", channel_id), ("guest_id", "=", guest_id)]
if channel_member_sudo and channel_member_sudo.env["discuss.channel.member"].search(domain, limit=1):
return (
request.env["ir.binary"]
._get_image_stream_from(guest_sudo, field_name="avatar_128", placeholder=placeholder)
.get_response()
)
if request.env.user.share:
return request.env["ir.binary"]._get_placeholder_stream(placeholder).get_response()
return (
request.env["ir.binary"]
._get_image_stream_from(guest_sudo.sudo(False), field_name="avatar_128", placeholder=placeholder)
.get_response()
)
if channel and guest and request.env["discuss.channel.member"].search(domain):
# sudo: mail.guest - the guest is in the same channel as the current user, so they can see their avatar
return request.env["ir.binary"]._get_image_stream_from(guest.sudo(), field_name="avatar_128").get_response()
return self.content_image(model="mail.guest", id=guest_id, field="avatar_128")
@http.route(
"/discuss/channel/<int:channel_id>/attachment/<int:attachment_id>", methods=["GET"], type="http", auth="public"
)
@add_guest_to_context
def discuss_channel_attachment(self, channel_id, attachment_id, download=None, **kwargs):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
domain = [
("id", "=", int(attachment_id)),
("res_id", "=", int(channel_id)),
("res_model", "=", "discuss.channel"),
]
attachment_sudo = channel_member_sudo.env["ir.attachment"].search(domain, limit=1)
# sudo: ir.attachment - searching for an attachment on a specific channel that the current user can access
attachment_sudo = request.env["ir.attachment"].sudo().search(domain)
if not attachment_sudo:
raise NotFound()
return request.env["ir.binary"]._get_stream_from(attachment_sudo).get_response(as_attachment=download)
@http.route(
"/discuss/channel/<int:channel_id>/avatar_128",
methods=["GET"],
type="http",
auth="public",
)
@http.route("/discuss/channel/<int:channel_id>/avatar_128", methods=["GET"], type="http", auth="public")
@add_guest_to_context
def discuss_channel_avatar_128(self, channel_id, **kwargs):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=channel_id)
domain = [("id", "=", channel_id)]
channel_sudo = channel_member_sudo.env["discuss.channel"].search(domain, limit=1)
if not channel_sudo:
raise NotFound()
return (
request.env["ir.binary"]
._get_image_stream_from(channel_sudo, field_name="avatar_128")
.get_response()
)
return self.content_image(model="discuss.channel", id=channel_id, field="avatar_128")
@http.route(
[
@@ -103,13 +75,16 @@ class BinaryController(http.Controller):
)
@add_guest_to_context
def fetch_image(self, channel_id, attachment_id, width=0, height=0, **kwargs):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=channel_id)
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
domain = [
("id", "=", attachment_id),
("res_id", "=", channel_id),
("res_model", "=", "discuss.channel"),
]
attachment_sudo = channel_member_sudo.env["ir.attachment"].search(domain, limit=1)
# sudo: ir.attachment - searching for an attachment on a specific channel that the current user can access
attachment_sudo = request.env["ir.attachment"].sudo().search(domain, limit=1)
if not attachment_sudo:
raise NotFound()
return (
+27 -36
View File
@@ -3,9 +3,7 @@
from werkzeug.exceptions import NotFound
from odoo import http
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import consteq
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
@@ -13,29 +11,10 @@ class ChannelController(http.Controller):
@http.route("/discuss/channel/members", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_members(self, channel_id, known_member_ids):
channel_member = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=channel_id)
return channel_member.channel_id.sudo().load_more_members(known_member_ids)
@http.route("/discuss/channel/add_guest_as_member", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_add_guest_as_member(self, channel_id, channel_uuid):
channel_sudo = request.env["discuss.channel"].browse(int(channel_id)).sudo().exists()
if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, channel_uuid):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
if channel_sudo.channel_type == "chat":
raise NotFound()
guest = channel_sudo.env["mail.guest"]._get_guest_from_context()
# Only guests should take this route.
if not guest:
raise NotFound()
channel_member = channel_sudo.env["discuss.channel.member"]._get_as_sudo_from_context(channel_id=channel_id)
# Do not add the guest to channel members if they are already member.
if not channel_member:
channel_sudo = channel_sudo.with_context(guest=guest)
try:
channel_sudo.add_members(guest_ids=[guest.id])
except UserError:
raise NotFound()
return channel.load_more_members(known_member_ids)
@http.route("/discuss/channel/update_avatar", methods=["POST"], type="json")
def discuss_channel_avatar_update(self, channel_id, data):
@@ -47,19 +26,22 @@ class ChannelController(http.Controller):
@http.route("/discuss/channel/info", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_info(self, channel_id):
member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context(channel_id=int(channel_id))
return member_sudo.channel_id._channel_info()
return request.env["discuss.channel"].search([("id", "=", channel_id)])._channel_info()
@http.route("/discuss/channel/messages", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_messages(self, channel_id, search_term=None, before=None, after=None, limit=30, around=None):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
domain = [
("res_id", "=", channel_id),
("model", "=", "discuss.channel"),
("message_type", "!=", "user_notification"),
]
res = channel_member_sudo.env["mail.message"]._message_fetch(domain, search_term=search_term, before=before, after=after, around=around, limit=limit)
res = request.env["mail.message"]._message_fetch(
domain, search_term=search_term, before=before, after=after, around=around, limit=limit
)
if not request.env.user._is_public() and not around:
res["messages"].set_message_done()
return {**res, "messages": res["messages"].message_format()}
@@ -67,20 +49,26 @@ class ChannelController(http.Controller):
@http.route("/discuss/channel/pinned_messages", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_pins(self, channel_id):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
return channel_member_sudo.channel_id.pinned_message_ids.sorted(key="pinned_at", reverse=True).message_format()
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
return channel.pinned_message_ids.sorted(key="pinned_at", reverse=True).message_format()
@http.route("/discuss/channel/set_last_seen_message", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_mark_as_seen(self, channel_id, last_message_id, allow_older=False):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
return channel_member_sudo.channel_id._channel_seen(last_message_id, allow_older=allow_older)
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
return channel._channel_seen(last_message_id, allow_older=allow_older)
@http.route("/discuss/channel/notify_typing", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_notify_typing(self, channel_id, is_typing):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
channel_member_sudo._notify_typing(is_typing)
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
if not member:
raise NotFound()
member._notify_typing(is_typing)
@http.route("/discuss/channel/attachments", methods=["POST"], type="json", auth="public")
@add_guest_to_context
@@ -91,11 +79,14 @@ class ChannelController(http.Controller):
:param limit: maximum number of attachments to return
:param before: id of the attachment from which to load older attachments
"""
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=channel_id)
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
domain = [
["res_id", "=", channel_id],
["res_model", "=", "discuss.channel"],
]
if before:
domain.append(["id", "<", before])
return channel_member_sudo.env["ir.attachment"].search(domain, limit=limit, order="id DESC")._attachment_format()
# sudo: ir.attachment - reading attachments of a channel that the current user can access
return request.env["ir.attachment"].sudo().search(domain, limit=limit, order="id DESC")._attachment_format()
+4
View File
@@ -2,6 +2,7 @@
import requests
import werkzeug.urls
from odoo.http import request, route, Controller
@@ -15,6 +16,7 @@ class DiscussGifController(Controller):
@route("/discuss/gif/search", type="json", auth="user")
def search(self, search_term, locale="en", country="US", position=None):
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
ir_config = request.env["ir.config_parameter"].sudo()
query_string = werkzeug.urls.url_encode(
{
@@ -35,6 +37,7 @@ class DiscussGifController(Controller):
@route("/discuss/gif/categories", type="json", auth="user")
def categories(self, locale="en", country="US"):
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
ir_config = request.env["ir.config_parameter"].sudo()
query_string = werkzeug.urls.url_encode(
{
@@ -55,6 +58,7 @@ class DiscussGifController(Controller):
request.env["discuss.gif.favorite"].create({"tenor_gif_id": tenor_gif_id})
def _gif_posts(self, ids):
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
ir_config = request.env["ir.config_parameter"].sudo()
query_string = werkzeug.urls.url_encode(
{
+28 -28
View File
@@ -5,8 +5,9 @@ from psycopg2.errorcodes import UNIQUE_VIOLATION
from werkzeug.exceptions import NotFound
from odoo import _, http
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import consteq
from odoo.tools import consteq, replace_exceptions
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
@@ -42,20 +43,25 @@ class PublicPageController(http.Controller):
@http.route("/chat/<int:channel_id>/<string:invitation_token>", methods=["GET"], type="http", auth="public")
@add_guest_to_context
def discuss_channel_invitation(self, channel_id, invitation_token):
channel_sudo = request.env["discuss.channel"].browse(channel_id).sudo().exists()
if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, invitation_token):
channel = request.env["discuss.channel"].browse(channel_id).exists()
# sudo: discuss.channel - channel access is validated with invitation_token
if not channel or not channel.sudo().uuid or not consteq(channel.sudo().uuid, invitation_token):
raise NotFound()
return self._response_discuss_channel_invitation(channel_sudo=channel_sudo)
return self._response_discuss_channel_invitation(channel)
@http.route("/discuss/channel/<int:channel_id>", methods=["GET"], type="http", auth="public")
@add_guest_to_context
def discuss_channel(self, channel_id):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
return self._response_discuss_public_template(channel_sudo=channel_member_sudo.channel_id)
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
raise NotFound()
return self._response_discuss_public_template(channel)
def _response_discuss_channel_from_token(self, create_token, channel_name=None, default_display_mode=False):
# sudo: ir.config_parameter - reading hard-coded key and using it in a simple condition
if not request.env["ir.config_parameter"].sudo().get_param("mail.chat_from_token"):
raise NotFound()
# sudo: discuss.channel - channel access is validated with invitation_token
channel_sudo = request.env["discuss.channel"].sudo().search([("uuid", "=", create_token)])
if not channel_sudo:
try:
@@ -75,49 +81,43 @@ class PublicPageController(http.Controller):
# commit the current transaction and get the channel.
request.env.cr.commit()
channel_sudo = channel_sudo.search([("uuid", "=", create_token)])
return self._response_discuss_channel_invitation(channel_sudo=channel_sudo, is_channel_token_secret=False)
return self._response_discuss_channel_invitation(channel_sudo.sudo(False), is_channel_token_secret=False)
def _response_discuss_channel_invitation(self, channel_sudo, is_channel_token_secret=True):
if channel_sudo.channel_type == "chat":
raise NotFound()
def _response_discuss_channel_invitation(self, channel, is_channel_token_secret=True):
discuss_public_view_data = {
"isChannelTokenSecret": is_channel_token_secret,
}
guest_already_known = channel_sudo.env["mail.guest"]._get_guest_from_context().exists()
__, guest = channel_sudo._find_or_create_persona_for_channel(
guest_name=_("Guest"),
country_code=request.geoip.country_code,
timezone=request.env['mail.guest']._get_timezone_from_request(request),
add_as_member=guest_already_known or not channel_sudo.env.user._is_public(),
)
guest_already_known = channel.env["mail.guest"]._get_guest_from_context()
with replace_exceptions(UserError, by=NotFound()):
# sudo: mail.guest - creating a guest and its member inside a channel of which they have the token
__, guest = channel.sudo()._find_or_create_persona_for_channel(
guest_name=_("Guest"),
country_code=request.geoip.country_code,
timezone=request.env["mail.guest"]._get_timezone_from_request(request),
)
if guest and not guest_already_known:
discuss_public_view_data.update(
{
"addGuestAsMemberOnJoin": True,
"shouldDisplayWelcomeViewInitially": True,
}
)
channel_sudo = channel_sudo.with_context(guest=guest)
response = self._response_discuss_public_template(
channel_sudo=channel_sudo, discuss_public_view_data=discuss_public_view_data
)
return response
channel = channel.with_context(guest=guest)
return self._response_discuss_public_template(channel, discuss_public_view_data=discuss_public_view_data)
def _response_discuss_public_template(self, channel_sudo, discuss_public_view_data=None):
def _response_discuss_public_template(self, channel, discuss_public_view_data=None):
discuss_public_view_data = discuss_public_view_data or {}
return request.render(
"mail.discuss_public_channel_template",
{
"data": {
"channelData": channel_sudo._channel_info()[0],
"channelData": channel._channel_info()[0],
"discussPublicViewData": dict(
{
"shouldDisplayWelcomeViewInitially": channel_sudo.default_display_mode
== "video_full_screen",
"shouldDisplayWelcomeViewInitially": channel.default_display_mode == "video_full_screen",
},
**discuss_public_view_data,
),
},
"session_info": channel_sudo.env["ir.http"].session_info(),
"session_info": channel.env["ir.http"].session_info(),
},
)
+27 -10
View File
@@ -1,6 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from werkzeug.exceptions import NotFound
from odoo import http
from odoo.http import request
@@ -23,7 +24,8 @@ class RtcController(http.Controller):
guest = request.env["mail.guest"]._get_guest_from_context()
notifications_by_session = defaultdict(list)
for sender_session_id, target_session_ids, content in peer_notifications:
session_sudo = guest.env["discuss.channel.rtc.session"].sudo().browse(int(sender_session_id)).exists()
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
session_sudo = request.env["discuss.channel.rtc.session"].sudo().browse(int(sender_session_id)).exists()
if (
not session_sudo
or (session_sudo.guest_id and session_sudo.guest_id != guest)
@@ -45,11 +47,13 @@ class RtcController(http.Controller):
if request.env.user._is_public():
guest = request.env["mail.guest"]._get_guest_from_context()
if guest:
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
session = guest.env["discuss.channel.rtc.session"].sudo().browse(int(session_id)).exists()
if session and session.guest_id == guest:
session._update_and_broadcast(values)
return
return
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
session = request.env["discuss.channel.rtc.session"].sudo().browse(int(session_id)).exists()
if session and session.partner_id == request.env.user.partner_id:
session._update_and_broadcast(values)
@@ -60,8 +64,11 @@ class RtcController(http.Controller):
"""Joins the RTC call of a channel if the user is a member of that channel
:param int channel_id: id of the channel to join
"""
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
return channel_member_sudo._rtc_join_call(check_rtc_session_ids=check_rtc_session_ids)
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
if not member:
raise NotFound()
# sudo: discuss.channel.rtc.session - member of current user can join call
return member.sudo()._rtc_join_call(check_rtc_session_ids=check_rtc_session_ids)
@http.route("/mail/rtc/channel/leave_call", methods=["POST"], type="json", auth="public")
@add_guest_to_context
@@ -69,8 +76,11 @@ class RtcController(http.Controller):
"""Disconnects the current user from a rtc call and clears any invitation sent to that user on this channel
:param int channel_id: id of the channel from which to disconnect
"""
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
return channel_member_sudo._rtc_leave_call()
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
if not member:
raise NotFound()
# sudo: discuss.channel.rtc.session - member of current user can leave call
return member.sudo()._rtc_leave_call()
@http.route("/mail/rtc/channel/cancel_call_invitation", methods=["POST"], type="json", auth="public")
@add_guest_to_context
@@ -79,8 +89,11 @@ class RtcController(http.Controller):
:param member_ids: members whose invitation is to cancel
:type member_ids: list(int) or None
"""
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=int(channel_id))
return channel_member_sudo.channel_id._rtc_cancel_invitations(member_ids=member_ids)
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
if not member:
raise NotFound()
# sudo: discuss.channel.rtc.session - member of current user can cancel invitations
return member.sudo().channel_id._rtc_cancel_invitations(member_ids=member_ids)
@http.route("/mail/rtc/audio_worklet_processor", methods=["GET"], type="http", auth="public")
def audio_worklet_processor(self):
@@ -92,18 +105,22 @@ class RtcController(http.Controller):
file_open("mail/static/src/worklets/audio_processor.js", "rb").read(),
headers=[
("Content-Type", "application/javascript"),
("Cache-Control", "max-age=%s" % http.STATIC_CACHE),
("Cache-Control", f"max-age={http.STATIC_CACHE}"),
],
)
@http.route("/discuss/channel/ping", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def channel_ping(self, channel_id, rtc_session_id=None, check_rtc_session_ids=None):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context(channel_id=int(channel_id))
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
if not member:
raise NotFound()
# sudo: discuss.channel.rtc.session - member of current user can access related sessions
channel_member_sudo = member.sudo()
if rtc_session_id:
domain = [
("id", "=", int(rtc_session_id)),
("channel_member_id", "=", channel_member_sudo.id),
("channel_member_id", "=", member.id),
]
channel_member_sudo.channel_id.rtc_session_ids.filtered_domain(domain).write({}) # update write_date
current_rtc_sessions, outdated_rtc_sessions = channel_member_sudo._rtc_sync_sessions(check_rtc_session_ids)
+3 -4
View File
@@ -11,8 +11,7 @@ class MessageReactionController(http.Controller):
@http.route("/mail/message/reaction", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def mail_message_add_reaction(self, message_id, content, action):
guest_sudo = request.env["mail.guest"]._get_guest_from_context().sudo()
message_sudo = guest_sudo.env["mail.message"].browse(int(message_id)).exists()
if not message_sudo._validate_access_for_current_persona("write"):
message = request.env["mail.message"].browse(int(message_id)).exists()
if not message._validate_access_for_current_persona("write"):
raise NotFound()
message_sudo._message_reaction(content, action)
message.sudo()._message_reaction(content, action)
+3 -1
View File
@@ -93,7 +93,9 @@ class ThreadController(http.Controller):
'last_used': datetime.now(),
'ids': canned_response_ids,
})
thread = request.env[thread_model]._get_from_context_or_raise(int(thread_id))
thread = request.env[thread_model].search([("id", "=", thread_id)])
if not thread:
raise NotFound()
if "body" in post_data:
post_data["body"] = Markup(post_data["body"]) # contains HTML such as @mentions
new_partners = []
+4 -4
View File
@@ -11,13 +11,13 @@ class WebclientController(http.Controller):
@http.route("/mail/init_messaging", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def mail_init_messaging(self):
if not request.env.user.sudo()._is_public():
return request.env.user.sudo(request.env.user.has_group("base.group_portal"))._init_messaging()
if not request.env.user._is_public():
return request.env.user.sudo(False)._init_messaging()
guest = request.env["mail.guest"]._get_guest_from_context()
if guest:
return guest.sudo()._init_messaging()
return guest._init_messaging()
raise NotFound()
@http.route("/mail/load_message_failures", methods=["POST"], type="json", auth="user")
def mail_load_message_failures(self):
return request.env.user.partner_id._message_fetch_failed()
return request.env.user.sudo(False).partner_id._message_fetch_failed()
+59 -136
View File
@@ -7,7 +7,6 @@ from hashlib import sha512
from markupsafe import Markup
from secrets import choice
from markupsafe import Markup
from werkzeug.exceptions import NotFound
from odoo import _, api, fields, models, tools, Command
from odoo.addons.base.models.avatar_mixin import get_hsl_from_seed
@@ -29,8 +28,6 @@ group_avatar = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.06 53
class Channel(models.Model):
""" A discuss.channel is a discussion group that may behave like a listener
on documents. """
_description = 'Discussion Channel'
_name = 'discuss.channel'
_mail_flat_thread = False
@@ -63,11 +60,8 @@ class Channel(models.Model):
channel_partner_ids = fields.Many2many(
'res.partner', string='Partners',
compute='_compute_channel_partner_ids', inverse='_inverse_channel_partner_ids',
compute_sudo=True, search='_search_channel_partner_ids',
groups='base.group_user')
channel_member_ids = fields.One2many(
'discuss.channel.member', 'channel_id', string='Members',
groups='base.group_user')
search='_search_channel_partner_ids')
channel_member_ids = fields.One2many('discuss.channel.member', 'channel_id', string='Members')
pinned_message_ids = fields.One2many('mail.message', 'res_id', domain=[('model', '=', 'discuss.channel'), ('pinned_at', '!=', False)], string='Pinned Messages')
sfu_channel_uuid = fields.Char(groups="base.group_system")
sfu_server_url = fields.Char(groups="base.group_system")
@@ -95,14 +89,16 @@ class Channel(models.Model):
# CONSTRAINTS
@api.constrains('channel_member_ids', 'channel_partner_ids')
@api.constrains('channel_member_ids')
def _constraint_partners_chat(self):
# sudo: discuss.channel - skipping ACL for constraint, more performant and no sensitive information is leaked
for ch in self.sudo().filtered(lambda ch: ch.channel_type == 'chat'):
if len(ch.channel_member_ids) > 2 or len(ch.channel_partner_ids) > 2:
if len(ch.channel_member_ids) > 2:
raise ValidationError(_("A channel of type 'chat' cannot have more than two users."))
@api.constrains('group_public_id', 'group_ids')
def _constraint_group_id_channel(self):
# sudo: discuss.channel - skipping ACL for constraint, more performant and no sensitive information is leaked
failing_channels = self.sudo().filtered(lambda channel: channel.channel_type != 'channel' and (channel.group_public_id or channel.group_ids))
if failing_channels:
raise ValidationError(_("For %(channels)s, channel_type should be 'channel' to have the group-based authorization or group auto-subscription.", channels=', '.join([ch.name for ch in failing_channels])))
@@ -118,9 +114,9 @@ class Channel(models.Model):
def _compute_is_editable(self):
for channel in self:
if channel.channel_type == 'channel':
channel.is_editable = self.sudo(False).env.is_admin() or channel.create_uid.id == self.env.user.id
channel.is_editable = self.env.user._is_admin() or channel.create_uid.id == self.env.user.id
elif channel.channel_type == 'group':
channel.is_editable = channel.is_member and not self.sudo(False).env.user._is_public()
channel.is_editable = channel.is_member and not self.env.user.share
else:
channel.is_editable = False
@@ -155,53 +151,27 @@ class Channel(models.Model):
'partner_id': partner.id,
} for partner in partners_new]
outdated += current_members.filtered(lambda m: m.partner_id not in partners)
if new_members:
self.env['discuss.channel.member'].create(new_members)
if outdated:
outdated.sudo().unlink()
outdated.unlink()
def _search_channel_partner_ids(self, operator, operand):
return [(
'channel_member_ids',
'in',
self.env['discuss.channel.member'].sudo()._search([
('partner_id', operator, operand)
])
)]
return [('channel_member_ids', 'any', [('partner_id', operator, operand)])]
@api.depends_context('uid', 'guest')
@api.depends('channel_member_ids')
def _compute_is_member(self):
if not self:
return
if self.env.user._is_public():
guest = self.env['mail.guest']._get_guest_from_context()
if not guest:
self.is_member = False
return
user_domain = [('guest_id', '=', guest.id)]
else:
user_domain = [('partner_id', '=', self.env.user.partner_id.id)]
members = self.env['discuss.channel.member'].sudo().search(expression.AND([[('channel_id', 'in', self.ids)], user_domain]))
is_member_channels = {member.channel_id for member in members}
members = self.env['discuss.channel.member'].search([('channel_id', 'in', self.ids), ('is_self', '=', True)])
is_member_channels = members.channel_id
for channel in self:
channel.is_member = channel in is_member_channels
def _search_is_member(self, operator, operand):
is_in = (operator == '=' and operand) or (operator == '!=' and not operand)
if self.env.user._is_public():
guest = self.env['mail.guest']._get_guest_from_context()
if not guest:
return expression.FALSE_DOMAIN if is_in else expression.TRUE_DOMAIN
user_domain = [('guest_id', '=', guest.id)]
else:
user_domain = [('partner_id', '=', self.env.user.partner_id.id)]
return [(
'channel_member_ids',
'in' if is_in else 'not in',
self.env['discuss.channel.member'].sudo()._search(user_domain)
)]
return [('channel_member_ids', "any" if is_in else "not any", [('is_self', '=', True)])]
@api.depends('channel_member_ids')
def _compute_member_count(self):
@@ -256,7 +226,8 @@ class Channel(models.Model):
# Create channel and alias
channels = super(Channel, self.with_context(mail_create_bypass_create_check=self.env['discuss.channel.member']._bypass_create_check, mail_create_nolog=True, mail_create_nosubscribe=True)).create(vals_list)
# pop the mail_create_bypass_create_check key to avoid leaking it outside of create)
channels = channels.with_context(mail_create_bypass_create_check=None)
channels._subscribe_users_automatically()
return channels
@@ -273,7 +244,7 @@ class Channel(models.Model):
def write(self, vals):
if 'channel_type' in vals:
failing_channels = self.sudo().filtered(lambda channel: channel.channel_type != vals.get('channel_type'))
failing_channels = self.filtered(lambda channel: channel.channel_type != vals.get('channel_type'))
if failing_channels:
raise UserError(_('Cannot change the channel type of: %(channel_names)s', channel_names=', '.join(failing_channels.mapped('name'))))
notifications = []
@@ -311,13 +282,6 @@ class Channel(models.Model):
if not self._cr.fetchone():
self._cr.execute('CREATE INDEX discuss_channel_member_seen_message_id_idx ON discuss_channel_member (channel_id,partner_id,seen_message_id)')
@api.model
def _get_from_context_or_raise(self, thread_id):
"""Overridden because guests and (portal) users need sudo to post on channels. This ensures they are actually
members before granting them access, as well as properly setting up the guest context on the resulting thread
if applicable."""
return self.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(thread_id).channel_id
# ------------------------------------------------------------
# MEMBERS MANAGEMENT
# ------------------------------------------------------------
@@ -330,6 +294,7 @@ class Channel(models.Model):
for channel_id in new_members
for partner_id in new_members[channel_id]
]
# sudo: discuss.channel.member - adding member of other users based on channel auto-subscribe
self.env['discuss.channel.member'].sudo().create(to_create)
def _subscribe_users_automatically_get_members(self):
@@ -344,22 +309,21 @@ class Channel(models.Model):
def _action_unfollow(self, partner):
self.message_unsubscribe(partner.ids)
if partner not in self.with_context(active_test=False).channel_partner_ids:
member = self.env['discuss.channel.member'].search([('channel_id', '=', self.id), ('partner_id', '=', partner.id)])
if not member:
return True
channel_info = self._channel_info()[0] # must be computed before leaving the channel (access rights)
member = self.env['discuss.channel.member'].search([('channel_id', '=', self.id), ('partner_id', '=', partner.id)])
member_id = member.id
member.unlink()
# side effect of unsubscribe that wasn't taken into account because
# channel_info is called before actually unpinning the channel
channel_info['is_pinned'] = False
self.env['bus.bus']._sendone(partner, 'discuss.channel/leave', channel_info)
notification = Markup('<div class="o_mail_notification">%s</div>') % _('left the channel')
# post 'channel left' message as root since the partner just unsubscribed from the channel
# sudo: mail.message - post as sudo since the user just unsubscribed from the channel
self.sudo().message_post(body=notification, subtype_xmlid="mail.mt_comment", author_id=partner.id)
self.env['bus.bus']._sendone(self, 'mail.record/insert', {
'Thread': {
'channelMembers': [('DELETE', {'id': member_id})],
'channelMembers': [('DELETE', {'id': member.id})],
'id': self.id,
'memberCount': self.member_count,
'model': "discuss.channel",
@@ -368,30 +332,12 @@ class Channel(models.Model):
def add_members(self, partner_ids=None, guest_ids=None, invite_to_rtc_call=False, open_chat_window=False, post_joined_message=True):
""" Adds the given partner_ids and guest_ids as member of self channels. """
self.check_access_rights('write')
self.check_access_rule('write')
current_partner, current_guest = self.env["res.partner"]._get_current_persona()
partners = self.env['res.partner'].browse(partner_ids or []).exists()
guests = self.env['mail.guest'].browse(guest_ids or []).exists()
notifications = []
for channel in self:
members_to_create = []
if channel.group_public_id:
invalid_partners = partners.filtered(lambda partner: channel.group_public_id not in partner.user_ids.groups_id)
if invalid_partners:
raise UserError(_(
'Channel "%(channel_name)s" only accepts members of group "%(group_name)s". Forbidden for: %(partner_names)s',
channel_name=channel.name,
group_name=channel.group_public_id.name,
partner_names=', '.join(partner.name for partner in invalid_partners)
))
if guests:
raise UserError(_(
'Channel "%(channel_name)s" only accepts members of group "%(group_name)s". Forbidden for: %(guest_names)s',
channel_name=channel.name,
group_name=channel.group_public_id.name,
guest_names=', '.join(guest.name for guest in guests)
))
existing_members = self.env['discuss.channel.member'].search(expression.AND([
[('channel_id', '=', channel.id)],
expression.OR([
@@ -407,17 +353,16 @@ class Channel(models.Model):
'guest_id': guest.id,
'channel_id': channel.id,
} for guest in guests - existing_members.guest_id]
new_members = self.env['discuss.channel.member'].sudo().create(members_to_create)
new_members = self.env['discuss.channel.member'].create(members_to_create)
for member in new_members.filtered(lambda member: member.partner_id):
# notify invited members through the bus
user = member.partner_id.user_ids[0] if member.partner_id.user_ids else self.env['res.users']
if user:
notifications.append((member.partner_id, 'discuss.channel/joined', {
'channel': member.channel_id.with_user(user).with_context(allowed_company_ids=user.company_ids.ids).sudo()._channel_info()[0],
'channel': member.channel_id.with_user(user).with_context(allowed_company_ids=user.company_ids.ids)._channel_info()[0],
'invited_by_user_id': self.env.user.id,
'open_chat_window': open_chat_window,
}))
if post_joined_message:
# notify existing members with a new message in the channel
if member.partner_id == self.env.user.partner_id:
@@ -432,7 +377,7 @@ class Channel(models.Model):
guest = member.guest_id
if guest:
notifications.append((guest, 'discuss.channel/joined', {
'channel': member.channel_id.sudo()._channel_info()[0],
'channel': member.channel_id.with_context(guest=guest)._channel_info()[0],
}))
notifications.append((channel, 'mail.record/insert', {
'Thread': {
@@ -442,7 +387,7 @@ class Channel(models.Model):
'model': "discuss.channel",
}
}))
if existing_members:
if existing_members and (current_partner or current_guest):
# If the current user invited these members but they are already present, notify the current user about their existence as well.
# In particular this fixes issues where the current user is not aware of its own member in the following case:
# create channel from form view, and then join from discuss without refreshing the page.
@@ -456,31 +401,13 @@ class Channel(models.Model):
}))
if invite_to_rtc_call:
for channel in self:
current_channel_member = self.env['discuss.channel.member'].sudo().search([('channel_id', '=', channel.id), ('partner_id', '=', current_partner.id), ('guest_id', '=', current_guest.id)])
if current_channel_member and current_channel_member.rtc_session_ids:
current_channel_member._rtc_invite_members(member_ids=new_members.ids)
current_channel_member = self.env['discuss.channel.member'].search([('channel_id', '=', channel.id), ('is_self', '=', 'True')])
# sudo: discuss.channel.rtc.session - reading rtc sessions of current user
if current_channel_member and current_channel_member.sudo().rtc_session_ids:
# sudo: discuss.channel.rtc.session - current user can invite new members in call
current_channel_member.sudo()._rtc_invite_members(member_ids=new_members.ids)
self.env['bus.bus']._sendmany(notifications)
def _can_invite(self, partner_id):
"""Return True if the current user can invite the partner to the channel.
* channel -- public channel: ok;
* -- group restricted channel: both current user and target must in the group;
* chat/group: current user must be member;
:return boolean: whether inviting is ok"""
partner = self.env['res.partner'].browse(partner_id)
for channel in self.sudo():
if channel.channel_type != 'channel' and not channel.is_member:
return False
if channel.group_public_id:
if not partner.user_ids or channel.group_public_id not in partner.user_ids.groups_id:
return False
if channel.group_public_id not in self.env.user.groups_id:
return False
return True
# ------------------------------------------------------------
# RTC
# ------------------------------------------------------------
@@ -538,9 +465,8 @@ class Channel(models.Model):
and use;
"""
# get values from msg_vals or from message if msg_vals doen't exists
msg_sudo = message.sudo()
message_type = msg_vals.get('message_type', 'email') if msg_vals else msg_sudo.message_type
pids = msg_vals.get('partner_ids', []) if msg_vals else msg_sudo.partner_ids.ids
message_type = msg_vals.get('message_type', 'email') if msg_vals else message.message_type
pids = msg_vals.get('partner_ids', []) if msg_vals else message.partner_ids.ids
# notify only user input (comment or incoming emails)
if message_type not in ('comment', 'email'):
@@ -549,8 +475,8 @@ class Channel(models.Model):
if not pids:
return []
email_from = tools.email_normalize(msg_vals.get('email_from') or msg_sudo.email_from)
author_id = msg_vals.get('author_id') or msg_sudo.author_id.id
email_from = tools.email_normalize(msg_vals.get('email_from') or message.email_from)
author_id = msg_vals.get('author_id') or message.author_id.id
recipients_data = []
if pids:
@@ -601,16 +527,15 @@ class Channel(models.Model):
message_format_values = message.message_format()[0]
bus_notifications = self._channel_message_notifications(message, message_format_values)
# Last interest and is_pinned are updated for a chat when posting a message.
# Last interest and is_pinned are updated for a channel when posting a message.
# So a notification is needed to update UI, and it should come before the
# notification of the message itself to ensure the channel automatically opens.
if self.is_chat or self.channel_type in ['channel', 'group']:
for member in self.channel_member_ids.filtered('partner_id'):
bus_notifications.insert(0, [member.partner_id, 'discuss.channel/last_interest_dt_changed', {
'id': self.id,
'isServerPinned': member.is_pinned,
'last_interest_dt': member.last_interest_dt,
}])
bus_notifications.insert(0, [self, 'discuss.channel/last_interest_dt_changed', {
'id': self.id,
'isServerPinned': True,
'last_interest_dt': fields.Datetime.now(),
}])
# sudo: bus.bus - sending on safe channel (target channel or partner)
self.env['bus.bus'].sudo()._sendmany(bus_notifications)
if self.is_chat or self.channel_type == 'group':
self._notify_thread_by_web_push(message, rdata, msg_vals, **kwargs)
@@ -639,11 +564,14 @@ class Channel(models.Model):
@api.returns('mail.message', lambda value: value.id)
def message_post(self, *, message_type='notification', **kwargs):
self.filtered(lambda channel: channel.is_chat or channel.channel_type in ['channel', 'group']).mapped('channel_member_ids').sudo().write({
if (not self.env.user or self.env.user._is_public()) and self.is_member:
# sudo: discuss.channel - guests don't have access for creating mail.message
self = self.sudo()
# sudo: discuss.channel.member - updating hard-coded fields/values for non-self members
self.sudo().channel_member_ids.write({
'is_pinned': True,
'last_interest_dt': fields.Datetime.now(),
})
# mail_post_autofollow=False is necessary to prevent adding followers
# when using mentions in channels. Followers should not be added to
# channels, and especially not automatically (because channel membership
@@ -772,30 +700,25 @@ class Channel(models.Model):
}
self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
def _find_or_create_persona_for_channel(self, guest_name, timezone, country_code, add_as_member=True, post_joined_message=True):
def _find_or_create_persona_for_channel(self, guest_name, timezone, country_code, post_joined_message=True):
"""
:param channel: channel to add the persona to
:param guest_name: name of the persona
:param add_as_member: whether to add the persona as a member of the channel
:param post_joined_message: whether to post a message to the channel
to notify that the persona joined
:return tuple(partner, guest):
"""
self.ensure_one()
guest = None
member = self.env["discuss.channel.member"]._get_as_sudo_from_context(channel_id=self.id)
member = self.env["discuss.channel.member"].search([("channel_id", "=", self.id), ("is_self", "=", True)])
if member:
return member.partner_id, member.guest_id
if not self.env.user._is_public() and add_as_member:
try:
self.add_members([self.env.user.partner_id.id], post_joined_message=post_joined_message)
except UserError:
raise NotFound()
if not self.env.user._is_public():
self.add_members([self.env.user.partner_id.id], post_joined_message=post_joined_message)
elif self.env.user._is_public():
is_guest_known = self.env["mail.guest"]._get_guest_from_context().exists()
country_id = self.env["res.country"].search([("code", "=", country_code)], limit=1).id
is_guest_known = self.env["mail.guest"]._get_guest_from_context()
country_id = self.env["res.country"].search([("code", "=", country_code)]).id
guest = self.env["mail.guest"]._find_or_create_for_channel(
add_as_member=add_as_member,
channel=self,
country_id=country_id,
name=guest_name,
@@ -814,6 +737,7 @@ class Channel(models.Model):
if not self:
return []
channel_infos = []
# sudo: discuss.channel.rtc.session - reading sessions of accessible channel is acceptable
rtc_sessions_by_channel = self.sudo().rtc_session_ids._mail_rtc_session_format_by_channel()
current_partner, current_guest = self.env["res.partner"]._get_current_persona()
self.env['discuss.channel'].flush_model()
@@ -837,14 +761,14 @@ class Channel(models.Model):
ORDER BY discuss_channel_member.id ASC
""", {'channel_ids': tuple(self.ids), 'current_partner_id': current_partner.id or None, 'current_guest_id': current_guest.id or None})
all_needed_members = self.env['discuss.channel.member'].browse([m['id'] for m in self.env.cr.dictfetchall()])
all_needed_members.partner_id.sudo().mail_partner_format() # prefetch in batch
all_needed_members._discuss_channel_member_format() # prefetch in batch
members_by_channel = defaultdict(lambda: self.env['discuss.channel.member'])
invited_members_by_channel = defaultdict(lambda: self.env['discuss.channel.member'])
member_of_current_user_by_channel = defaultdict(lambda: self.env['discuss.channel.member'])
for member in all_needed_members:
members_by_channel[member.channel_id] |= member
members_by_channel[member.channel_id] += member
if member.rtc_inviting_session_id:
invited_members_by_channel[member.channel_id] |= member
invited_members_by_channel[member.channel_id] += member
if (current_partner and member.partner_id == current_partner) or (current_guest and member.guest_id == current_guest):
member_of_current_user_by_channel[member.channel_id] = member
for channel in self:
@@ -976,7 +900,6 @@ class Channel(models.Model):
else:
# create a new one
channel = self.create({
'channel_partner_ids': [Command.link(partner_id) for partner_id in partners_to],
'channel_member_ids': [
Command.create({
'partner_id': partner_id,
@@ -985,7 +908,7 @@ class Channel(models.Model):
}) for partner_id in partners_to
],
'channel_type': 'chat',
'name': ', '.join(self.env['res.partner'].sudo().browse(partners_to).mapped('name')),
'name': ', '.join(self.env['res.partner'].browse(partners_to).mapped('name')),
})
channel._broadcast(partners_to)
return channel
@@ -1133,7 +1056,7 @@ class Channel(models.Model):
self.write({'description': description})
def channel_join(self):
""" Shortcut to add the current user as member of self channels.
"""Shortcut to add the current user as member of self channels.
Prefer calling add_members() directly when possible.
"""
self.add_members(self.env.user.partner_id.ids)
@@ -1179,7 +1102,7 @@ class Channel(models.Model):
'default_display_mode': default_display_mode,
'name': name,
})
channel._broadcast(partners_to)
channel._broadcast(channel.channel_member_ids.partner_id.ids)
return channel
@api.model
@@ -1,12 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.exceptions import NotFound
from odoo import api, fields, models, _
from odoo.exceptions import AccessError, ValidationError
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.osv import expression
class ChannelMember(models.Model):
_name = "discuss.channel.member"
_description = "Channel Member"
@@ -16,6 +13,7 @@ class ChannelMember(models.Model):
# identity
partner_id = fields.Many2one("res.partner", "Partner", ondelete="cascade", index=True)
guest_id = fields.Many2one("mail.guest", "Guest", ondelete="cascade", readonly=True, index=True)
is_self = fields.Boolean(compute="_compute_is_self", search="_search_is_self")
# channel
channel_id = fields.Many2one("discuss.channel", "Channel", ondelete="cascade", readonly=True, required=True)
# state
@@ -38,7 +36,34 @@ class ChannelMember(models.Model):
if any(user._is_public() for user in member.partner_id.user_ids):
raise ValidationError(_("Channel members cannot include public users."))
@api.depends('channel_id.message_ids', 'seen_message_id')
@api.depends_context("uid", "guest")
def _compute_is_self(self):
if not self:
return
current_partner, current_guest = self.env["res.partner"]._get_current_persona()
self.is_self = False
for member in self:
if current_partner and member.partner_id == current_partner:
member.is_self = True
if current_guest and member.guest_id == current_guest:
member.is_self = True
def _search_is_self(self, operator, operand):
is_in = (operator == "=" and operand) or (operator == "!=" and not operand)
current_partner, current_guest = self.env["res.partner"]._get_current_persona()
if is_in:
return [
'|',
("partner_id", "=", current_partner.id) if current_partner else expression.FALSE_LEAF,
("guest_id", "=", current_guest.id) if current_guest else expression.FALSE_LEAF,
]
else:
return [
("partner_id", "!=", current_partner.id) if current_partner else expression.TRUE_LEAF,
("guest_id", "!=", current_guest.id) if current_guest else expression.TRUE_LEAF,
]
@api.depends("channel_id.message_ids", "seen_message_id")
def _compute_message_unread(self):
if self.ids:
self.env['mail.message'].flush_model()
@@ -83,58 +108,35 @@ class ChannelMember(models.Model):
@api.model_create_multi
def create(self, vals_list):
"""Similar access rule as the access rule of the mail channel.
It can not be implemented in XML, because when the record will be created, the
partner will be added in the channel and the security rule will always authorize
the creation.
"""
if not self.env.is_admin() and not self.env.context.get('mail_create_bypass_create_check') is self._bypass_create_check:
for vals in vals_list:
if 'channel_id' in vals:
channel_id = self.env['discuss.channel'].browse(vals['channel_id'])
if not channel_id._can_invite(vals.get('partner_id')):
raise AccessError(_('This user can not be added in this channel'))
if self.env.context.get("mail_create_bypass_create_check") is self._bypass_create_check:
self = self.sudo()
for vals in vals_list:
if "channel_id" not in vals:
raise UserError(
_(
"It appears you're trying to create a channel member, but it seems like you forgot to specify the related channel. "
"To move forward, please make sure to provide the necessary channel information."
)
)
channel = self.env["discuss.channel"].browse(vals["channel_id"])
if channel.channel_type == "chat" and len(channel.channel_member_ids) > 0:
raise UserError(
_("Adding more members to this chat isn't possible; it's designed for just two people.")
)
return super().create(vals_list)
def write(self, vals):
for channel_member in self:
for field_name in {'channel_id', 'partner_id', 'guest_id'}:
for field_name in ['channel_id', 'partner_id', 'guest_id']:
if field_name in vals and vals[field_name] != channel_member[field_name].id:
raise AccessError(_('You can not write on %(field_name)s.', field_name=field_name))
return super().write(vals)
def unlink(self):
self.sudo().rtc_session_ids.unlink()
# sudo: discuss.channel.rtc.session - cascade unlink of sessions for self member
self.sudo().rtc_session_ids.unlink() # ensure unlink overrides are applied
return super().unlink()
@api.model
def _get_as_sudo_from_context_or_raise(self, channel_id):
channel_member = self._get_as_sudo_from_context(channel_id=channel_id)
if not channel_member:
raise NotFound()
return channel_member
@api.model
def _get_as_sudo_from_context(self, channel_id):
""" Seeks a channel member matching the provided `channel_id` and the
current user or guest.
:param channel_id: The id of the channel of which the user/guest is
expected to be member.
:type channel_id: int
:return: A record set containing the channel member if found, or an
empty record set otherwise. In case of guest, the record is returned
with the 'guest' record in the context.
:rtype: discuss.channel.member
"""
if self.env.uid and not self.env.user._is_public():
return self.env['discuss.channel.member'].sudo().search([('channel_id', '=', channel_id), ('partner_id', '=', self.env.user.partner_id.id)], limit=1)
guest = self.env['mail.guest']._get_guest_from_context()
if guest:
return guest.env['discuss.channel.member'].sudo().search([('channel_id', '=', channel_id), ('guest_id', '=', guest.id)], limit=1)
return self.env['discuss.channel.member'].sudo()
def _notify_typing(self, is_typing):
""" Broadcast the typing notification to channel members
:param is_typing: (boolean) tells whether the members are typing or not
@@ -159,9 +161,11 @@ class ChannelMember(models.Model):
data['thread'] = member.channel_id._channel_format(fields=fields.get('channel')).get(member.channel_id)
if 'persona' in fields:
if member.partner_id:
persona = member._get_partner_data(fields=fields.get('persona', {}).get('partner'))
# sudo: res.partner - reading _get_partner_data related to a member is considered acceptable
persona = member.sudo()._get_partner_data(fields=fields.get('persona', {}).get('partner'))
persona['type'] = "partner"
if member.guest_id:
# sudo: mail.guest - reading _guest_format related to a member is considered acceptable
persona = member.guest_id.sudo()._guest_format(fields=fields.get('persona', {}).get('guest')).get(member.guest_id)
data['persona'] = persona
members_formatted_data[member] = data
@@ -20,6 +20,7 @@ class IrAttachment(models.Model):
def _attachment_format(self):
attachment_format = super()._attachment_format()
for a in attachment_format:
# sudo: discuss.voice.metadata - checking the existence of voice metadata for accessible attachments is fine
a["voice"] = bool(self.browse(a["id"]).with_prefetch(self._prefetch_ids).sudo().voice_ids)
return attachment_format
+12 -13
View File
@@ -1,3 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
@@ -8,6 +10,7 @@ class IrWebsocket(models.AbstractModel):
def _get_im_status(self, data):
im_status = super()._get_im_status(data)
if "mail.guest" in data:
# sudo: mail.guest - necessary to read im_status from other guests, information is not considered sensitive
im_status["Persona"] += [{**g, 'type': "guest"} for g in (
self.env["mail.guest"]
.sudo()
@@ -19,26 +22,22 @@ class IrWebsocket(models.AbstractModel):
@add_guest_to_context
def _build_bus_channel_list(self, channels):
channels = list(channels) # do not alter original list
guest_sudo = self.env["mail.guest"]._get_guest_from_context().sudo()
discuss_channels = self.env["discuss.channel"]
if self.env.uid and not self.env.user._is_public():
discuss_channels = self.env.user.partner_id.channel_ids
elif guest_sudo:
discuss_channels = guest_sudo.channel_ids
channels.append(guest_sudo)
for discuss_channel in discuss_channels:
channels.append(discuss_channel)
guest = self.env["mail.guest"]._get_guest_from_context()
if guest:
channels.append(guest)
channels.extend(self.env["discuss.channel"].search([("is_member", "=", True)]))
return super()._build_bus_channel_list(channels)
@add_guest_to_context
def _update_bus_presence(self, inactivity_period, im_status_ids_by_model):
super()._update_bus_presence(inactivity_period, im_status_ids_by_model)
if not self.env.user or self.env.user._is_public():
guest_sudo = self.env["mail.guest"]._get_guest_from_context().sudo()
if not guest_sudo:
guest = self.env["mail.guest"]._get_guest_from_context()
if not guest:
return
guest_sudo.env["bus.presence"].update_presence(
# sudo: bus.presence - guests currently need sudo to write their own presence
self.env["bus.presence"].sudo().update_presence(
inactivity_period,
identity_field="guest_id",
identity_value=guest_sudo.id,
identity_value=guest.id,
)
+16 -17
View File
@@ -5,7 +5,6 @@ import uuid
from datetime import datetime, timedelta
from functools import wraps
from inspect import Parameter, signature
from werkzeug.exceptions import NotFound
from odoo.tools import consteq, get_lang
from odoo import _, api, fields, models
@@ -83,6 +82,7 @@ class MailGuest(models.Model):
parts = token.split(self._cookie_separator)
if len(parts) == 2:
guest_id, guest_access_token = parts
# sudo: mail.guest: guests need sudo to read their access_token
guest = self.browse(int(guest_id)).sudo().exists()
if not guest or not guest.access_token or not consteq(guest.access_token, guest_access_token):
guest = self.env["mail.guest"]
@@ -92,7 +92,7 @@ class MailGuest(models.Model):
"""Returns the current guest record from the context, if applicable."""
guest = self.env.context.get('guest')
if isinstance(guest, self.pool['mail.guest']):
return guest.with_context(guest=guest)
return guest.sudo(False).with_context(guest=guest)
return self.env['mail.guest']
def _get_timezone_from_request(self, request):
@@ -129,20 +129,25 @@ class MailGuest(models.Model):
def _init_messaging(self):
self.ensure_one()
odoobot = self.env.ref('base.partner_root')
# sudo: res.partner - exposing OdooBot name and id
odoobot = self.env.ref('base.partner_root').sudo()
# sudo: mail.guest - guest reading their own id/name/channels
guest_sudo = self.sudo()
return {
'channels': self.channel_ids._channel_info(),
'channels': guest_sudo.channel_ids.sudo(False)._channel_info(),
'companyName': self.env.company.name,
'currentGuest': {
'id': self.id,
'name': self.name,
'id': guest_sudo.id,
'name': guest_sudo.name,
'type': "guest",
},
'current_partner': False,
'current_user_id': False,
'current_user_settings': False,
# sudo: ir.config_parameter: safe to check for existence of tenor api key
'hasGifPickerFeature': bool(self.env["ir.config_parameter"].sudo().get_param("discuss.tenor_api_key")),
'hasLinkPreviewFeature': self.env['mail.link.preview']._is_link_preview_enabled(),
# sudo: bus.bus: reading non-sensitive last id
'initBusId': self.env['bus.bus'].sudo()._bus_last_id(),
'menu_id': False,
'needaction_inbox_counter': False,
@@ -171,7 +176,7 @@ class MailGuest(models.Model):
guests_formatted_data[guest] = data
return guests_formatted_data
def _find_or_create_for_channel(self, channel, name, country_id, timezone, add_as_member=True, post_joined_message=False):
def _find_or_create_for_channel(self, channel, name, country_id, timezone, post_joined_message=False):
"""Get a guest for the given channel. If there is no guest yet,
create one.
@@ -183,8 +188,6 @@ class MailGuest(models.Model):
:param post_joined_message: whether to post a message to the channel
to notify that the guest joined
"""
if channel.group_public_id:
raise NotFound()
guest = self._get_guest_from_context()
if not guest:
guest = self.create(
@@ -194,19 +197,14 @@ class MailGuest(models.Model):
"name": name,
"timezone": timezone,
}
)
if add_as_member:
channel = channel.with_context(guest=guest)
try:
channel.add_members(guest_ids=[guest.id], post_joined_message=post_joined_message)
except UserError:
raise NotFound()
).sudo(False)
channel.add_members(guest_ids=guest.ids, post_joined_message=post_joined_message)
return guest
def _set_auth_cookie(self):
"""Add a cookie to the response to identify the guest. Every route
that expects a guest will make use of it to authenticate the guest
through `_get_as_sudo_from_context` or `_get_as_sudo_from_context_or_raise`.
through `add_guest_to_context`.
"""
self.ensure_one()
expiration_date = datetime.now() + timedelta(days=365)
@@ -216,6 +214,7 @@ class MailGuest(models.Model):
httponly=True,
expires=expiration_date,
)
request.update_context(guest=self.sudo(False))
def _format_auth_cookie(self):
"""Format the cookie value for the given guest.
@@ -12,6 +12,7 @@ class MailMessage(models.Model):
self.ensure_one()
if self.env.user._is_public():
guest = self.env["mail.guest"]._get_guest_from_context()
# sudo: mail.guest - current guest can read channels they are member of
return guest and self.model == "discuss.channel" and self.res_id in guest.sudo().channel_ids.ids
return super()._validate_access_for_current_persona(operation)
-31
View File
@@ -16,37 +16,6 @@ class ResPartner(models.Model):
copy=False,
)
def _get_channels_as_member(self):
"""Returns the channels of the partner."""
self.ensure_one()
channels = self.env["discuss.channel"]
# get the channels and groups
channels |= self.env["discuss.channel"].search(
[
("channel_type", "in", ("channel", "group")),
("channel_partner_ids", "in", [self.id]),
]
)
# get the pinned direct messages (directly include whatsapp to avoid overrides)
channels |= self.env["discuss.channel"].search(
[
("channel_type", "in", ["chat", "whatsapp"]),
(
"channel_member_ids",
"in",
self.env["discuss.channel.member"]
.sudo()
._search(
[
("partner_id", "=", self.id),
("is_pinned", "=", True),
]
),
),
]
)
return channels
@api.model
def search_for_channel_invite(self, search_term, channel_id=None, limit=30):
"""Returns partners matching search_term that can be invited to a channel.
+13 -5
View File
@@ -38,11 +38,9 @@ class ResUsers(models.Model):
def _unsubscribe_from_non_public_channels(self):
"""This method un-subscribes users from group restricted channels. Main purpose
of this method is to prevent sending internal communication to archived / deleted users.
We do not un-subscribes users from public channels because in most common cases,
public channels are mailing list (e-mail based) and so users should always receive
updates from public channels until they manually un-subscribe themselves.
"""
domain = [("partner_id", "in", self.partner_id.ids)]
# sudo: discuss.channel.member - removing member of other users based on channel restrictions
current_cm = self.env["discuss.channel.member"].sudo().search(domain)
current_cm.filtered(
lambda cm: (cm.channel_id.channel_type == "channel" and cm.channel_id.group_public_id)
@@ -50,8 +48,18 @@ class ResUsers(models.Model):
def _init_messaging(self):
self.ensure_one()
# 2 different queries because the 2 sub-queries together with OR are less efficient
channels = self.env["discuss.channel"].with_user(self)
channels += channels.search([("channel_type", "in", ("channel", "group")), ("is_member", "=", True)])
channels += channels.search(
[
("channel_type", "not in", ("channel", "group")),
("channel_member_ids", "any", [("is_self", "=", True), ("is_pinned", "=", True)]),
]
)
return {
'channels': self.partner_id._get_channels_as_member()._channel_info(),
'hasGifPickerFeature': bool(self.env["ir.config_parameter"].sudo().get_param("discuss.tenor_api_key")),
"channels": channels._channel_info(),
# sudo: ir.config_parameter - reading hard-coded key to check its existence, safe to return if the feature is enabled
"hasGifPickerFeature": bool(self.env["ir.config_parameter"].sudo().get_param("discuss.tenor_api_key")),
**super()._init_messaging(),
}
+2 -1
View File
@@ -62,7 +62,8 @@ class IrAttachment(models.Model):
def _delete_and_notify(self, message=None):
if message:
message.write({}) # to make sure write_date on the message is updated
# sudo: mail.message - safe write just updating the date, because guests don't have the rights
message.sudo().write({}) # to make sure write_date on the message is updated
self.env['bus.bus']._sendmany((attachment._bus_notification_target(), 'ir.attachment/delete', {
'id': attachment.id, 'message': {'id': message.id, 'write_date': message.write_date} if message else None
}) for attachment in self)
-8
View File
@@ -22,7 +22,6 @@ from email.message import EmailMessage
from email import message_from_string
from lxml import etree
from werkzeug import urls
from werkzeug.exceptions import NotFound
from xmlrpc import client as xmlrpclib
from markupsafe import Markup, escape
@@ -479,13 +478,6 @@ class MailThread(models.AbstractModel):
if any(message.message_type != 'comment' for message in messages):
raise exceptions.UserError(_("Only messages type comment can have their content updated"))
@api.model
def _get_from_context_or_raise(self, thread_id):
thread = self.search([["id", "=", thread_id]])
if not thread:
raise NotFound()
return thread
# ------------------------------------------------------
# TRACKING / LOG
# ------------------------------------------------------
+2 -4
View File
@@ -6,7 +6,6 @@ import re
from odoo import _, api, fields, models, tools
from odoo.osv import expression
class Partner(models.Model):
""" Update partner to add a field about notification preferences. Add a generic opt-out field that can be used
to restrict usage of automatic email templates. """
@@ -319,7 +318,6 @@ class Partner(models.Model):
@api.model
def _get_current_persona(self):
guest = self.env["mail.guest"]._get_guest_from_context()
if self.env.user._is_public() and guest:
return (self.env["res.partner"], guest)
if not self.env.user or self.env.user._is_public():
return (self.env["res.partner"], self.env["mail.guest"]._get_guest_from_context())
return (self.env.user.partner_id, self.env["mail.guest"])
+7 -26
View File
@@ -3,40 +3,29 @@ access_fetchmail_server,fetchmail.server,model_fetchmail_server,base.group_syste
access_mail_message_all,mail.message.all,model_mail_message,base.group_public,1,0,0,0
access_mail_message_portal,mail.message.portal,model_mail_message,base.group_portal,1,1,1,1
access_mail_message_user,mail.message.user,model_mail_message,base.group_user,1,1,1,1
access_mail_message_scheduled_all,mail.message.scheduled.all,model_mail_message_schedule,,0,0,0,0
access_mail_message_scheduled_system,mail.message.scheduled.system,model_mail_message_schedule,base.group_system,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,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,,0,0,0,0
access_mail_followers_user,mail.followers.user,model_mail_followers,base.group_user,1,0,0,0
access_mail_followers_system,mail.followers.system,model_mail_followers,base.group_system,1,1,1,1
access_mail_notification_portal,mail.notification.portal,model_mail_notification,base.group_portal,1,0,0,0
access_mail_notification_user,mail.notification.user,model_mail_notification,base.group_user,1,1,1,0
access_mail_notification_system, mail.notification.system,model_mail_notification,base.group_system,1,1,1,1
access_discuss_channel_public,discuss.channel.all,model_discuss_channel,base.group_public,1,0,0,0
access_discuss_channel_portal,discuss.channel.all,model_discuss_channel,base.group_portal,1,0,0,0
access_discuss_channel_public,discuss.channel.public,model_discuss_channel,base.group_public,1,0,0,0
access_discuss_channel_portal,discuss.channel.portal,model_discuss_channel,base.group_portal,1,0,0,0
access_discuss_channel_user,discuss.channel.user,model_discuss_channel,base.group_user,1,1,1,0
access_discuss_channel_admin,discuss.channel.system,model_discuss_channel,base.group_system,1,1,1,1
access_discuss_channel_member_public,discuss.channel.member.public,model_discuss_channel_member,base.group_public,1,0,0,0
access_discuss_channel_system,discuss.channel.system,model_discuss_channel,base.group_system,1,1,1,1
access_discuss_channel_member_public,discuss.channel.member.public,model_discuss_channel_member,base.group_public,1,1,1,1
access_discuss_channel_member_portal,discuss.channel.member.portal,model_discuss_channel_member,base.group_portal,1,1,1,1
access_discuss_channel_member_user,discuss.channel.member.user,model_discuss_channel_member,base.group_user,1,1,1,1
access_discuss_channel_rtc_session_all,discuss.channel.rtc.session.all,model_discuss_channel_rtc_session,,0,0,0,0
access_discuss_channel_rtc_session_system,discuss.channel.rtc.session.system,model_discuss_channel_rtc_session,base.group_system,1,1,1,1
access_mail_alias_all,mail.alias.all,model_mail_alias,,0,0,0,0
access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,0,0,0
access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1
access_mail_gateway_allowed_system,mail.gateway.allowed.system,model_mail_gateway_allowed,base.group_system,1,1,1,1
access_mail_message_reaction_all,mail.message.reaction.all,model_mail_message_reaction,,0,0,0,0
access_mail_message_reaction_system,mail.message.reaction.system,model_mail_message_reaction,base.group_system,1,1,1,1
access_mail_message_subtype_public,mail.message.subtype.all,model_mail_message_subtype,base.group_public,1,0,0,0
access_mail_message_subtype_portal,mail.message.subtype.all,model_mail_message_subtype,base.group_portal,1,0,0,0
access_mail_message_subtype_user,mail.message.subtype.user,model_mail_message_subtype,base.group_user,1,0,0,0
access_mail_message_subtype_system,mail.message.subtype.system,model_mail_message_subtype,base.group_system,1,1,1,1
access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value,,0,0,0,0
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_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,base.group_system,1,1,1,1
access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,1
@@ -44,17 +33,12 @@ access_mail_template_editor,mail.template_editor,model_mail_template,mail.group_
access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1
access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1,1
access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0
access_mail_activity_all,mail.activity.all,model_mail_activity,,0,0,0,0
access_mail_activity_user,mail.activity.user,model_mail_activity,base.group_user,1,1,1,1
access_mail_activity_plan_all,mail.activity.plan.all,model_mail_activity_plan,,0,0,0,0
access_mail_activity_plan_user,mail.activity.plan.user,model_mail_activity_plan,base.group_user,1,0,0,0
access_mail_activity_plan_system,mail.activity.plan.system,model_mail_activity_plan,base.group_system,1,1,1,1
access_mail_activity_plan_template_all,mail.activity.plan.template.all,model_mail_activity_plan_template,,0,0,0,0
access_mail_activity_plan_template_user,mail.activity.plan.template.user,model_mail_activity_plan_template,base.group_user,1,0,0,0
access_mail_activity_plan_template_system,mail.activity.plan.template.system,model_mail_activity_plan_template,base.group_system,1,1,1,1
access_mail_activity_schedule_all,mail.activity.schedule.all,model_mail_activity_schedule,,0,0,0,0
access_mail_activity_schedule_user,mail.activity.schedule.user,model_mail_activity_schedule,base.group_user,1,1,1,0
access_mail_activity_type_all,mail.activity.type.all,model_mail_activity_type,,0,0,0,0
access_mail_activity_type_user,mail.activity.type.user,model_mail_activity_type,base.group_user,1,0,0,0
access_mail_activity_type_system,mail.activity.type.system,model_mail_activity_type,base.group_system,1,1,1,1
access_mail_blacklist_system,access_mail_blacklist_system,model_mail_blacklist,base.group_system,1,1,1,1
@@ -65,17 +49,14 @@ access_mail_resend_message,access.mail.resend.message,model_mail_resend_message,
access_mail_resend_partner,access.mail.resend.partner,model_mail_resend_partner,base.group_user,1,1,1,0
access_mail_template_preview,access.mail.template.preview,model_mail_template_preview,base.group_user,1,1,1,0
access_mail_blacklist_remove_system,acesss.mail.blacklist.remove.system,model_mail_blacklist_remove,base.group_system,1,1,1,1
access_mail_guest_all,mail.guest,model_mail_guest,,0,0,0,0
access_mail_guest_user,mail.guest,model_mail_guest,base.group_user,1,0,0,0
access_mail_guest_system,mail.guest,model_mail_guest,base.group_system,1,1,1,1
access_mail_ice_server_all,mail.ice.server.all,model_mail_ice_server,,0,0,0,0
access_mail_ice_server_system,mail.ice.server.system,model_mail_ice_server,base.group_system,1,1,1,1
access_res_users_settings_volumes_all,res.users.settings.volumes,model_res_users_settings_volumes,,0,0,0,0
access_res_users_settings_volumes_user,res.users.settings.volumes,model_res_users_settings_volumes,base.group_user,1,1,1,1
access_mail_template_reset,access.mail.template.reset,model_mail_template_reset,mail.group_mail_template_editor,1,1,1,1
ir_actions_report_access_user,ir.actions.report.access.user,base.model_ir_actions_report,base.group_user,1,0,0,0
access_mail_link_preview_admin,mail.link.preview.admin,model_mail_link_preview,base.group_erp_manager,1,1,1,1
access_discuss_gif_favorite,discuss.gif.favorite,model_discuss_gif_favorite,base.group_user,1,1,1,1
access_discuss_voice_metadata_user,discuss.voice.metadata.user,model_discuss_voice_metadata,,0,0,0,0
access_mail_partner_device,access_mail_partner_device,mail.model_mail_partner_device,base.group_user,0,0,0,0
access_mail_notification_web_push,access_mail_notification_web_push,mail.model_mail_notification_web_push,base.group_user,0,0,0,0
access_discuss_voice_metadata_user,discuss.voice.metadata.user,model_discuss_voice_metadata,base.group_system,1,1,1,1
access_mail_partner_device,access_mail_partner_device,mail.model_mail_partner_device,base.group_system,1,1,1,1
access_mail_notification_web_push,access_mail_notification_web_push,mail.model_mail_notification_web_push,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_mail_message_all mail.message.all model_mail_message base.group_public 1 0 0 0
4 access_mail_message_portal mail.message.portal model_mail_message base.group_portal 1 1 1 1
5 access_mail_message_user mail.message.user model_mail_message base.group_user 1 1 1 1
access_mail_message_scheduled_all mail.message.scheduled.all model_mail_message_schedule 0 0 0 0
6 access_mail_message_scheduled_system mail.message.scheduled.system model_mail_message_schedule base.group_system 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 0 0 0 0
7 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 0 0 0 0
8 access_mail_followers_user mail.followers.user model_mail_followers base.group_user 1 0 0 0
9 access_mail_followers_system mail.followers.system model_mail_followers base.group_system 1 1 1 1
10 access_mail_notification_portal mail.notification.portal model_mail_notification base.group_portal 1 0 0 0
11 access_mail_notification_user mail.notification.user model_mail_notification base.group_user 1 1 1 0
12 access_mail_notification_system mail.notification.system model_mail_notification base.group_system 1 1 1 1
13 access_discuss_channel_public discuss.channel.all discuss.channel.public model_discuss_channel base.group_public 1 0 0 0
14 access_discuss_channel_portal discuss.channel.all discuss.channel.portal model_discuss_channel base.group_portal 1 0 0 0
15 access_discuss_channel_user discuss.channel.user model_discuss_channel base.group_user 1 1 1 0
16 access_discuss_channel_admin access_discuss_channel_system discuss.channel.system model_discuss_channel base.group_system 1 1 1 1
17 access_discuss_channel_member_public discuss.channel.member.public model_discuss_channel_member base.group_public 1 0 1 0 1 0 1
18 access_discuss_channel_member_portal discuss.channel.member.portal model_discuss_channel_member base.group_portal 1 1 1 1
19 access_discuss_channel_member_user discuss.channel.member.user model_discuss_channel_member base.group_user 1 1 1 1
access_discuss_channel_rtc_session_all discuss.channel.rtc.session.all model_discuss_channel_rtc_session 0 0 0 0
20 access_discuss_channel_rtc_session_system discuss.channel.rtc.session.system model_discuss_channel_rtc_session base.group_system 1 1 1 1
access_mail_alias_all mail.alias.all model_mail_alias 0 0 0 0
21 access_mail_alias_user mail.alias.user model_mail_alias base.group_user 1 0 0 0
22 access_mail_alias_system mail.alias.system model_mail_alias base.group_system 1 1 1 1
23 access_mail_gateway_allowed_system mail.gateway.allowed.system model_mail_gateway_allowed base.group_system 1 1 1 1
access_mail_message_reaction_all mail.message.reaction.all model_mail_message_reaction 0 0 0 0
24 access_mail_message_reaction_system mail.message.reaction.system model_mail_message_reaction base.group_system 1 1 1 1
25 access_mail_message_subtype_public mail.message.subtype.all model_mail_message_subtype base.group_public 1 0 0 0
26 access_mail_message_subtype_portal mail.message.subtype.all model_mail_message_subtype base.group_portal 1 0 0 0
27 access_mail_message_subtype_user mail.message.subtype.user model_mail_message_subtype base.group_user 1 0 0 0
28 access_mail_message_subtype_system mail.message.subtype.system model_mail_message_subtype base.group_system 1 1 1 1
access_mail_tracking_value_all mail.tracking.value.all model_mail_tracking_value 0 0 0 0
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
29 access_mail_tracking_value_system mail.tracking.value.system model_mail_tracking_value base.group_system 1 1 1 1
30 access_publisher_warranty_contract_all publisher.warranty.contract.all model_publisher_warranty_contract base.group_system 1 1 1 1
31 access_mail_template mail.template model_mail_template base.group_user 1 1 1 1
33 access_mail_template_system mail.template_system model_mail_template base.group_system 1 1 1 1
34 access_mail_shortcode mail.shortcode model_mail_shortcode base.group_user 1 1 1 1
35 access_mail_shortcode_portal mail.shortcode.portal model_mail_shortcode base.group_portal 1 0 0 0
access_mail_activity_all mail.activity.all model_mail_activity 0 0 0 0
36 access_mail_activity_user mail.activity.user model_mail_activity base.group_user 1 1 1 1
access_mail_activity_plan_all mail.activity.plan.all model_mail_activity_plan 0 0 0 0
37 access_mail_activity_plan_user mail.activity.plan.user model_mail_activity_plan base.group_user 1 0 0 0
38 access_mail_activity_plan_system mail.activity.plan.system model_mail_activity_plan base.group_system 1 1 1 1
access_mail_activity_plan_template_all mail.activity.plan.template.all model_mail_activity_plan_template 0 0 0 0
39 access_mail_activity_plan_template_user mail.activity.plan.template.user model_mail_activity_plan_template base.group_user 1 0 0 0
40 access_mail_activity_plan_template_system mail.activity.plan.template.system model_mail_activity_plan_template base.group_system 1 1 1 1
access_mail_activity_schedule_all mail.activity.schedule.all model_mail_activity_schedule 0 0 0 0
41 access_mail_activity_schedule_user mail.activity.schedule.user model_mail_activity_schedule base.group_user 1 1 1 0
access_mail_activity_type_all mail.activity.type.all model_mail_activity_type 0 0 0 0
42 access_mail_activity_type_user mail.activity.type.user model_mail_activity_type base.group_user 1 0 0 0
43 access_mail_activity_type_system mail.activity.type.system model_mail_activity_type base.group_system 1 1 1 1
44 access_mail_blacklist_system access_mail_blacklist_system model_mail_blacklist base.group_system 1 1 1 1
49 access_mail_resend_partner access.mail.resend.partner model_mail_resend_partner base.group_user 1 1 1 0
50 access_mail_template_preview access.mail.template.preview model_mail_template_preview base.group_user 1 1 1 0
51 access_mail_blacklist_remove_system acesss.mail.blacklist.remove.system model_mail_blacklist_remove base.group_system 1 1 1 1
access_mail_guest_all mail.guest model_mail_guest 0 0 0 0
52 access_mail_guest_user mail.guest model_mail_guest base.group_user 1 0 0 0
53 access_mail_guest_system mail.guest model_mail_guest base.group_system 1 1 1 1
access_mail_ice_server_all mail.ice.server.all model_mail_ice_server 0 0 0 0
54 access_mail_ice_server_system mail.ice.server.system model_mail_ice_server base.group_system 1 1 1 1
access_res_users_settings_volumes_all res.users.settings.volumes model_res_users_settings_volumes 0 0 0 0
55 access_res_users_settings_volumes_user res.users.settings.volumes model_res_users_settings_volumes base.group_user 1 1 1 1
56 access_mail_template_reset access.mail.template.reset model_mail_template_reset mail.group_mail_template_editor 1 1 1 1
57 ir_actions_report_access_user ir.actions.report.access.user base.model_ir_actions_report base.group_user 1 0 0 0
58 access_mail_link_preview_admin mail.link.preview.admin model_mail_link_preview base.group_erp_manager 1 1 1 1
59 access_discuss_gif_favorite discuss.gif.favorite model_discuss_gif_favorite base.group_user 1 1 1 1
60 access_discuss_voice_metadata_user discuss.voice.metadata.user model_discuss_voice_metadata base.group_system 0 1 0 1 0 1 0 1
61 access_mail_partner_device access_mail_partner_device mail.model_mail_partner_device base.group_user base.group_system 0 1 0 1 0 1 0 1
62 access_mail_notification_web_push access_mail_notification_web_push mail.model_mail_notification_web_push base.group_user base.group_system 0 1 0 1 0 1 0 1
+157 -47
View File
@@ -1,28 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- RULES -->
<record id="discuss_channel_rule" model="ir.rule">
<field name="name">discuss.channel: access only public and joined groups</field>
<record id="ir_rule_discuss_channel_all" model="ir.rule">
<field name="name">discuss.channel: can access channels (as member or as group allowed)</field>
<field name="model_id" ref="mail.model_discuss_channel"/>
<field name="groups" eval="[Command.link(ref('base.group_user')), Command.link(ref('base.group_portal')), Command.link(ref('base.group_public'))]"/>
<field name="domain_force">[
'|',
'&amp;',
('channel_type', '!=', 'channel'),
('is_member', '=', True),
'&amp;',
('channel_type', '=', 'channel'),
'|',
('group_public_id', '=', False),
('group_public_id', 'in', [g.id for g in user.groups_id])]
<field name="groups"
eval="[
Command.link(ref('base.group_user')),
Command.link(ref('base.group_portal')),
Command.link(ref('base.group_public')),
]"
/>
<field name="domain_force">
[
"|",
"&amp;",
("channel_type", "!=", "channel"),
("is_member", "=", True),
"&amp;",
("channel_type", "=", "channel"),
'|',
('group_public_id', '=', False),
('group_public_id', 'in', user.groups_id.ids)
]
</field>
<field name="perm_create" eval="False"/><!-- create follows access rules (allowed for internal and system) -->
<field name="perm_unlink" eval="False"/><!-- unlink follows access rules (allowed for system) -->
</record>
<record id="ir_rule_discuss_channel_group_system" model="ir.rule">
<field name="name">discuss.channel: admin full access</field>
<field name="model_id" ref="mail.model_discuss_channel"/>
<field name="groups" eval="[Command.link(ref('base.group_system'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<record id="ir_rule_discuss_channel_member_is_self_all" model="ir.rule">
<field name="name">discuss.channel.member: access their own entries</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups"
eval="[
Command.link(ref('base.group_user')),
Command.link(ref('base.group_portal')),
Command.link(ref('base.group_public')),
]"
/>
<field name="domain_force">
[
('is_self', '=', True),
"|",
"&amp;",
("channel_id.channel_type", "!=", "channel"),
("channel_id.is_member", "=", True),
"&amp;",
("channel_id.channel_type", "=", "channel"),
'|',
('channel_id.group_public_id', '=', False),
('channel_id.group_public_id', 'in', user.groups_id.ids)
]
</field>
<!--
create() is controlled by other rules because create() rules are applied after the record contains
its data, which means just using 'is_self' would allow any user to add themselves in any channel.
-->
<field name="perm_create" eval="False"/>
</record>
<record id="discuss_channel_admin" model="ir.rule">
<field name="name">discuss.channel: admin full access</field>
<field name="model_id" ref="mail.model_discuss_channel"/>
<record id="ir_rule_discuss_channel_member_read_all" model="ir.rule">
<field name="name">discuss.channel.member: read members of accessible channels</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups"
eval="[
Command.link(ref('base.group_user')),
Command.link(ref('base.group_portal')),
Command.link(ref('base.group_public')),
]"
/>
<field name="domain_force">
[
"|",
"&amp;",
("channel_id.channel_type", "!=", "channel"),
("channel_id.is_member", "=", True),
"&amp;",
("channel_id.channel_type", "=", "channel"),
'|',
('channel_id.group_public_id', '=', False),
('channel_id.group_public_id', 'in', user.groups_id.ids)
]
</field>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_discuss_channel_member_create_is_group_matching_all" model="ir.rule">
<field name="name">discuss.channel.member: can join group restricted channels when group is matching</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups"
eval="[
Command.link(ref('base.group_user')),
Command.link(ref('base.group_portal')),
Command.link(ref('base.group_public')),
]"
/>
<field name="domain_force">
[
('is_self', '=', True),
('channel_id.channel_type', '=', 'channel'),
'|',
('channel_id.group_public_id', '=', False),
('channel_id.group_public_id', 'in', user.groups_id.ids)
]
</field>
<!--
This is the only case where the current user can join themselves (is_self = True) when the channel
is already created, in all other cases they must be invited by someone else.
-->
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_discuss_channel_member_create_is_group_matching_group_user" model="ir.rule">
<field name="name">discuss.channel.member: internal users can invite others in group restricted channels when group is matching</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups" eval="[Command.link(ref('base.group_user'))]"/>
<field name="domain_force">
[
('is_self', '=', False),
('channel_id.channel_type', '=', 'channel'),
'|',
('channel_id.group_public_id', '=', False),
('channel_id.group_public_id', 'in', user.groups_id.ids)
]
</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_discuss_channel_member_create_is_member_group_user" model="ir.rule">
<field name="name">discuss.channel.member: internal users can invite others in channels they are member of</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups" eval="[Command.link(ref('base.group_user'))]"/>
<field name="domain_force">
[
('is_self', '=', False),
('channel_id.channel_type', 'not in', ('channel', 'chat')),
('channel_id.is_member', '=', True)
]
</field>
<!--
create() for the current user is controlled by other rules because create() rules are applied after the record
contains its data, which means allowing 'is_self' would allow any user to add themselves in any channel.
-->
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_discuss_channel_member_group_system" model="ir.rule">
<field name="name">discuss.channel.member: admin can manipulate all entries</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups" eval="[Command.link(ref('base.group_system'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
@@ -41,34 +180,6 @@
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<record id="ir_rule_discuss_channel_member_group_user" model="ir.rule">
<field name="name">discuss.channel.member: write its own entries</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
<field name="domain_force">[
'|',
'&amp;',
('channel_id.channel_type', '!=', 'channel'),
('channel_id.is_member', '=', True),
'&amp;',
('channel_id.channel_type', '=', 'channel'),
'|',
('channel_id.group_public_id', '=', False),
('channel_id.group_public_id', 'in', [g.id for g in user.groups_id])]
</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="True"/>
</record>
<record id="ir_rule_discuss_channel_member_group_system" model="ir.rule">
<field name="name">discuss.channel.member: admin can manipulate all entries</field>
<field name="model_id" ref="mail.model_discuss_channel_member"/>
<field name="groups" eval="[Command.link(ref('base.group_system'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<record id="ir_rule_mail_notifications_group_user" model="ir.rule">
<field name="name">mail.notifications: group_user: write its own entries</field>
<field name="model_id" ref="model_mail_notification"/>
@@ -164,5 +275,4 @@
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
</odoo>
@@ -32,12 +32,6 @@ export class WelcomePage extends Component {
if (this.store.guest) {
await this.personaService.updateGuestName(this.store.self, this.state.userName.trim());
}
if (this.props.data?.discussPublicViewData.addGuestAsMemberOnJoin) {
await this.rpc("/discuss/channel/add_guest_as_member", {
channel_id: this.props.data.channelData.id,
channel_uuid: this.props.data.channelData.uuid,
});
}
this.props.proceed?.();
}
+2 -5
View File
@@ -1097,12 +1097,8 @@ class MailCase(MockEmail):
}, {...}]
"""
bus_notifs = self.env['bus.bus'].sudo().search([('channel', 'in', [json_dump(channel) for channel in channels])])
if check_unique:
self.assertEqual(len(bus_notifs), len(channels))
self.assertEqual(set(bus_notifs.mapped('channel')), set([json_dump(channel) for channel in channels]))
notif_messages = [n.message for n in bus_notifs]
for expected in message_items or []:
for notification in notif_messages:
if json_dump(expected) == notification:
@@ -1110,7 +1106,8 @@ class MailCase(MockEmail):
else:
raise AssertionError('No notification was found with the expected value.\nExpected:\n%s\nReturned:\n%s' %
(json_dump(expected), '\n'.join([n for n in notif_messages])))
if check_unique:
self.assertEqual(len(bus_notifs), len(channels))
return bus_notifs
def assertNotified(self, message, recipients_info, is_complete=False):
+1
View File
@@ -1,6 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_discuss_channel
from . import test_discuss_channel_access
from . import test_discuss_channel_as_guest
from . import test_discuss_channel_member
from . import test_message_controller
+25 -204
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
@@ -9,200 +8,13 @@ from odoo import Command, fields
from odoo.addons.mail.models.discuss.discuss_channel import channel_avatar, group_avatar
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common import MailCommon
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests import tagged, Form
from odoo.tests.common import users
from odoo.exceptions import ValidationError
from odoo.tests.common import tagged, users
from odoo.tools import html_escape, mute_logger
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
class TestChannelAccessRights(MailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_employee_1 = mail_new_test_user(cls.env, login='user_employee_1', groups='base.group_user', name='Tao Lee')
cls.user_public = mail_new_test_user(cls.env, login='user_public', groups='base.group_public', name='Bert Tartignole')
cls.user_portal = mail_new_test_user(cls.env, login='user_portal', groups='base.group_portal', name='Chell Gladys')
# Channel for certain group
cls.group_restricted_channel = cls.env['discuss.channel'].channel_create(name='Channel for Groups', group_id=cls.env.ref('base.group_user').id)
# Public Channel
cls.public_channel = cls.env['discuss.channel'].channel_create(name='Public Channel', group_id=None)
# Group
cls.private_group = cls.env['discuss.channel'].create_group(partners_to=cls.user_employee.partner_id.ids, name="Group")
# Chat
cls.chat_user_employee = cls.env['discuss.channel'].channel_get(cls.user_employee.partner_id.ids)
cls.chat_user_employee_1 = cls.env['discuss.channel'].channel_get(cls.user_employee_1.partner_id.ids)
cls.chat_user_portal = cls.env['discuss.channel'].channel_get(cls.user_portal.partner_id.ids)
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model', 'odoo.models')
@users('user_public')
def test_access_public(self):
# Read public channel -> ok
self.env['discuss.channel'].browse(self.public_channel.id).read()
# Read group restricted channel -> ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.group_restricted_channel.id).read()
# Read group -> ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.private_group.id).read()
# Being a member of public channel: -> ko
with self.assertRaises(ValidationError):
self.public_channel.add_members(self.user_public.partner_id.id)
# Being a member of group restricted channel: -> ko, no access rights
with self.assertRaises(UserError):
self.group_restricted_channel.add_members(self.user_public.partner_id.id)
# Being a group member: -> ko
with self.assertRaises(ValidationError):
self.private_group.add_members(self.user_public.partner_id.id)
# Create channel/group/chat: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].create({'name': 'Test', 'channel_type': 'channel'})
with self.assertRaises(AccessError):
self.env['discuss.channel'].create({'name': 'Test', 'channel_type': 'group'})
with self.assertRaises(AccessError):
self.env['discuss.channel'].create({'name': 'Test', 'channel_type': 'chat'})
# Update channel/group/chat: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.public_channel.id).write({'name': 'modified'})
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.group_restricted_channel.id).write({'name': 'modified'})
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.private_group.id).write({'name': 'modified'})
# Unlink channel/group/chat: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.public_channel.id).unlink()
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.group_restricted_channel.id).unlink()
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.private_group.id).unlink()
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model', 'odoo.models')
@users('employee')
def test_access_employee(self):
# Read public channel -> ok
self.env['discuss.channel'].browse(self.public_channel.id).read()
# Read group restricted channel -> ok
self.env['discuss.channel'].browse(self.group_restricted_channel.id).read()
# Read chat when not being a member: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.chat_user_employee_1.id).read()
# Update chat when not being a member: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.chat_user_employee_1.id).write({'name': 'modified'})
# Being a channel/group member: -> ok
self.public_channel.add_members(self.env.user.partner_id.id)
self.group_restricted_channel.add_members(self.env.user.partner_id.id)
self.private_group.add_members(self.env.user.partner_id.id)
# Read a group when being a member: ok
self.env['discuss.channel'].browse(self.private_group.id).read()
# Read a chat when being a member: ok
self.env['discuss.channel'].browse(self.chat_user_employee.id).read()
# Update channel/group/chat when being a member: ok
self.env['discuss.channel'].browse(self.public_channel.id).write({'name': 'modified again'})
self.env['discuss.channel'].browse(self.group_restricted_channel.id).write({'name': 'modified again'})
self.env['discuss.channel'].browse(self.private_group.id).write({'name': 'modified again'})
self.env['discuss.channel'].browse(self.chat_user_employee.id).write({'name': 'modified again'})
# Create channel/group/chat: ok
new_channel = self.env['discuss.channel'].create(
{'name': 'Test', 'channel_type': 'channel'})
new_group = self.env['discuss.channel'].create(
{'name': 'Test', 'channel_type': 'group'})
new_chat = self.env['discuss.channel'].create(
{'name': 'Test', 'channel_type': 'chat'})
# Employee should be inside the created chat/group/chat
self.assertIn(new_channel.channel_partner_ids, self.partner_employee)
self.assertIn(new_group.channel_partner_ids, self.partner_employee)
self.assertIn(new_chat.channel_partner_ids, self.partner_employee)
# Unlink channel/group/chat: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.public_channel.id).unlink()
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.group_restricted_channel.id).unlink()
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.private_group.id).unlink()
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.chat_user_employee.id).unlink()
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model', 'odoo.models')
@users('user_portal')
def test_access_portal(self):
# Read public channel -> ok
self.env['discuss.channel'].browse(self.public_channel.id).read()
# Read group restricted channel/group -> ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.group_restricted_channel.id).read()
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.private_group.id).read()
# Being a group member: -> ok
self.private_group.add_members(self.user_portal.partner_id.id)
# Read a group/chat when being a member: ok
self.env['discuss.channel'].browse(self.private_group.id).read()
self.env['discuss.channel'].browse(self.chat_user_portal.id).read()
# Update group/chat when being a member: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.private_group.id).write({'name': 'modified'})
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.chat_user_portal.id).write({'name': 'modified'})
# Create group/chat: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].create({'name': 'Test', 'channel_type': 'group'})
with self.assertRaises(AccessError):
self.env['discuss.channel'].create({'name': 'Test', 'channel_type': 'chat'})
# Unlink group/chat: ko, no access rights
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.private_group.id).unlink()
with self.assertRaises(AccessError):
self.env['discuss.channel'].browse(self.chat_user_portal.id).unlink()
# Read message from group/chat: ok
group_portal = self.env['discuss.channel'].browse(self.private_group.id)
for message in group_portal.message_ids:
message.read(['subject'])
chat_portal = self.env['discuss.channel'].browse(self.chat_user_portal.id)
for message in chat_portal.message_ids:
message.read(['subject'])
# Read partner list from group: ko, no access rights
with self.assertRaises(AccessError):
group_portal.message_partner_ids
for partner in self.private_group.message_partner_ids:
if partner.id == self.user_portal.partner_id.id:
# Portal user can read their own partner record
continue
with self.assertRaises(AccessError):
partner.with_user(self.user_portal).name
# Read partner list from chat: ko, no access rights
with self.assertRaises(AccessError):
chat_portal.message_partner_ids
for partner in self.chat_user_portal.message_partner_ids:
if partner.id == self.user_portal.partner_id.id:
# Portal user can read their own partner record
continue
with self.assertRaises(AccessError):
partner.with_user(self.user_portal).name
@tagged("post_install", "-at_install")
class TestChannelInternals(MailCommon):
@classmethod
@@ -224,6 +36,13 @@ class TestChannelInternals(MailCommon):
)
cls.partner_employee_nomail = cls.user_employee_nomail.partner_id
def test_channel_member_cannot_be_public_user(self):
"""Public users can only join channels as guest."""
user_public = mail_new_test_user(self.env, login='user_public', groups='base.group_public', name='Bert Tartignole')
public_channel = self.env['discuss.channel'].channel_create(name='Public Channel', group_id=None)
with self.assertRaises(ValidationError):
public_channel.add_members(user_public.partner_id.id)
@users('employee')
def test_channel_members(self):
channel = self.env['discuss.channel'].browse(self.test_channel.ids)
@@ -270,7 +89,8 @@ class TestChannelInternals(MailCommon):
def test_channel_recipients_channel(self):
""" Posting a message on a channel should not send emails """
channel = self.env['discuss.channel'].browse(self.test_channel.ids)
channel.add_members((self.partner_employee | self.partner_admin | self.test_partner).ids)
# sudo: discuss.channel.member - adding members in non-accessible channel in a test file
channel.sudo().add_members((self.partner_employee | self.partner_admin | self.test_partner).ids)
with self.mock_mail_gateway():
new_msg = channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment')
self.assertNotSentEmail()
@@ -431,19 +251,20 @@ class TestChannelInternals(MailCommon):
public_channel = self.env['discuss.channel'].channel_create(name='Channel for Everyone', group_id=None)
private_group = self.env['discuss.channel'].create_group(partners_to=self.user_employee.partner_id.ids, name="Group")
chat_user_current = self.env['discuss.channel'].channel_get(self.env.user.partner_id.ids)
group_restricted_channel.add_members(self.env.user.partner_id.id)
public_channel.add_members(self.env.user.partner_id.id)
self.assertEqual(len(group_restricted_channel.channel_member_ids), 1)
self.assertEqual(len(public_channel.channel_member_ids), 1)
self.assertEqual(len(private_group.sudo().channel_member_ids), 1)
self.assertEqual(len(chat_user_current.sudo().channel_member_ids), 1)
group_restricted_channel.action_unfollow()
public_channel.action_unfollow()
private_group.action_unfollow()
chat_user_current.action_unfollow()
self.assertEqual(group_restricted_channel.channel_partner_ids, self.env['res.partner'])
self.assertEqual(public_channel.channel_partner_ids, self.env['res.partner'])
self.assertEqual(private_group.channel_partner_ids, self.env['res.partner'])
self.assertEqual(chat_user_current.channel_partner_ids, self.env['res.partner'])
self.assertEqual(len(group_restricted_channel.channel_member_ids), 0)
self.assertEqual(len(public_channel.channel_member_ids), 0)
# sudo: mail.channel - reading members of non-accessible channel for testing purposes
self.assertEqual(len(private_group.sudo().channel_member_ids), 0)
# sudo: mail.channel - reading members of non-accessible channel for testing purposes
self.assertEqual(len(chat_user_current.sudo().channel_member_ids), 0)
def test_channel_unfollow_should_not_post_message_if_the_partner_has_been_removed(self):
'''
@@ -569,10 +390,10 @@ class TestChannelInternals(MailCommon):
self.assertEqual(self.env.user.company_id, self.company_admin)
with self.with_user('employee'):
initial_channel_info = self.env['discuss.channel'].with_context(
chat = self.env['discuss.channel'].with_context(
allowed_company_ids=self.company_admin.ids
).channel_get(self.partner_employee_c2.ids)._channel_info()[0]
self.assertTrue(initial_channel_info, 'should be able to chat with multi company user')
).channel_get(self.partner_employee_c2.ids)
self.assertTrue(chat, 'should be able to chat with multi company user')
@users('employee')
def test_create_chat_channel_should_only_pin_the_channel_for_the_current_user(self):
@@ -0,0 +1,484 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests.common import tagged
from odoo.tools import mute_logger
@tagged("post_install", "-at_install")
class TestDiscussChannelAccess(MailCommon):
def test_01_discuss_channel_access(self):
"""Exhaustive list of operations on channel that would make sense in various setup and check
whether or not they can be executed.
Combinations that make no sense have not been listed, such as create a channel while already
being member of the channel.
"""
self.secret_group = self.env["res.groups"].create({"name": "Secret User Group"})
self.env["ir.model.data"].create(
{
"name": "secret_group",
"module": "mail",
"model": self.secret_group._name,
"res_id": self.secret_group.id,
}
)
self.guest = self.env["mail.guest"].create({"name": "A Guest"}).sudo(False)
self.users = {
"public": mail_new_test_user(
self.env, login="public1", name="A Public User", groups="base.group_public,mail.secret_group"
),
"portal": mail_new_test_user(
self.env, login="portal1", name="A Portal User", groups="base.group_portal,mail.secret_group"
),
"user": mail_new_test_user(
self.env, login="user1", name="An Internal User", groups="base.group_user,mail.secret_group"
),
}
self.other_user = mail_new_test_user(
self.env, login="other1", name="Another User 1", groups="base.group_user,mail.secret_group"
)
tests = [
("public", "no_group", "member", "read", True),
("public", "no_group", "member", "write", False),
("public", "no_group", "member", "unlink", False),
("public", "no_group", "outside", "create", False),
("public", "no_group", "outside", "read", True),
("public", "no_group", "outside", "write", False),
("public", "no_group", "outside", "unlink", False),
("public", "group_matching", "member", "read", True),
("public", "group_matching", "member", "write", False),
("public", "group_matching", "member", "unlink", False),
("public", "group_matching", "outside", "create", False),
("public", "group_matching", "outside", "read", True),
("public", "group_matching", "outside", "write", False),
("public", "group_matching", "outside", "unlink", False),
("public", "group_failing", "member", "read", False),
("public", "group_failing", "member", "write", False),
("public", "group_failing", "member", "unlink", False),
("public", "group_failing", "outside", "create", False),
("public", "group_failing", "outside", "read", False),
("public", "group_failing", "outside", "write", False),
("public", "group_failing", "outside", "unlink", False),
("public", "group", "member", "read", True),
("public", "group", "member", "write", False),
("public", "group", "member", "unlink", False),
("public", "group", "outside", "create", False),
("public", "group", "outside", "read", False),
("public", "group", "outside", "write", False),
("public", "group", "outside", "unlink", False),
("public", "chat", "outside", "create", False),
("public", "chat", "outside", "read", False),
("public", "chat", "outside", "write", False),
("public", "chat", "outside", "unlink", False),
("portal", "no_group", "member", "read", True),
("portal", "no_group", "member", "write", False),
("portal", "no_group", "member", "unlink", False),
("portal", "no_group", "outside", "create", False),
("portal", "no_group", "outside", "read", True),
("portal", "no_group", "outside", "write", False),
("portal", "no_group", "outside", "unlink", False),
("portal", "group_matching", "member", "read", True),
("portal", "group_matching", "member", "write", False),
("portal", "group_matching", "member", "unlink", False),
("portal", "group_matching", "outside", "create", False),
("portal", "group_matching", "outside", "read", True),
("portal", "group_matching", "outside", "write", False),
("portal", "group_matching", "outside", "unlink", False),
("portal", "group_failing", "member", "read", False),
("portal", "group_failing", "member", "write", False),
("portal", "group_failing", "member", "unlink", False),
("portal", "group_failing", "outside", "create", False),
("portal", "group_failing", "outside", "read", False),
("portal", "group_failing", "outside", "write", False),
("portal", "group_failing", "outside", "unlink", False),
("portal", "group", "member", "read", True),
("portal", "group", "member", "write", False),
("portal", "group", "member", "unlink", False),
("portal", "group", "outside", "create", False),
("portal", "group", "outside", "read", False),
("portal", "group", "outside", "write", False),
("portal", "group", "outside", "unlink", False),
("portal", "chat", "member", "read", True),
("portal", "chat", "member", "write", False),
("portal", "chat", "member", "unlink", False),
("portal", "chat", "outside", "create", False),
("portal", "chat", "outside", "read", False),
("portal", "chat", "outside", "write", False),
("portal", "chat", "outside", "unlink", False),
("user", "no_group", "member", "read", True),
("user", "no_group", "member", "write", True),
("user", "no_group", "member", "unlink", False),
("user", "no_group", "outside", "create", True),
("user", "no_group", "outside", "read", True),
("user", "no_group", "outside", "write", True),
("user", "no_group", "outside", "unlink", False),
("user", "group_matching", "member", "read", True),
("user", "group_matching", "member", "write", True),
("user", "group_matching", "member", "unlink", False),
("user", "group_matching", "outside", "create", True),
("user", "group_matching", "outside", "read", True),
("user", "group_matching", "outside", "write", True),
("user", "group_matching", "outside", "unlink", False),
("user", "group_failing", "member", "read", False),
("user", "group_failing", "member", "write", False),
("user", "group_failing", "member", "unlink", False),
("user", "group_failing", "outside", "create", True),
("user", "group_failing", "outside", "read", False),
("user", "group_failing", "outside", "write", False),
("user", "group_failing", "outside", "unlink", False),
("user", "group", "member", "read", True),
("user", "group", "member", "write", True),
("user", "group", "member", "unlink", False),
("user", "group", "outside", "create", True),
("user", "group", "outside", "read", False),
("user", "group", "outside", "write", False),
("user", "group", "outside", "unlink", False),
("user", "chat", "member", "read", True),
("user", "chat", "member", "write", True),
("user", "chat", "member", "unlink", False),
("user", "chat", "outside", "create", True),
("user", "chat", "outside", "read", False),
("user", "chat", "outside", "write", False),
("user", "chat", "outside", "unlink", False),
]
for user_key, channel_key, membership, operation, result in tests:
if result:
try:
self._execute_action_channel(user_key, channel_key, membership, operation, result)
except Exception as e: # noqa: BLE001 - re-raising, just with a more contextual message
raise AssertionError(f"{user_key, channel_key, membership, operation} should not raise") from e
else:
try:
with self.assertRaises(Exception), mute_logger("odoo.sql_db"), mute_logger(
"odoo.addons.base.models.ir_rule"
):
self._execute_action_channel(user_key, channel_key, membership, operation, result)
except AssertionError as e:
raise AssertionError(f"{user_key, channel_key, membership, operation} should raise") from e
def test_10_discuss_channel_member_access(self):
"""Exhaustive list of operations on channel.member that would make sense in various setup
and check whether or not they can be executed.
Combinations that make no sense have not been listed, such as reading self-member when not
being member, or having a chat (type) as a guest (which is a current restriction).
"""
self.secret_group = self.env["res.groups"].create({"name": "Secret User Group"})
self.env["ir.model.data"].create(
{
"name": "secret_group",
"module": "mail",
"model": self.secret_group._name,
"res_id": self.secret_group.id,
}
)
self.guest = self.env["mail.guest"].create({"name": "A Guest"}).sudo(False)
self.users = {
"public": mail_new_test_user(
self.env, login="public1", name="A Public User", groups="base.group_public,mail.secret_group"
),
"portal": mail_new_test_user(
self.env, login="portal1", name="A Portal User", groups="base.group_portal,mail.secret_group"
),
"user": mail_new_test_user(
self.env, login="user1", name="An Internal User", groups="base.group_user,mail.secret_group"
),
}
self.other_user = mail_new_test_user(
self.env, login="other1", name="Another User 1", groups="base.group_user,mail.secret_group"
)
self.other_user_2 = mail_new_test_user(
self.env, login="other2", name="Another User 2", groups="base.group_user,mail.secret_group"
)
tests = [
("public", "no_group", "member", "self", "create", False),
("public", "no_group", "member", "self", "read", True),
("public", "no_group", "member", "self", "write", True),
("public", "no_group", "member", "self", "unlink", True),
("public", "no_group", "member", "other", "create", False),
("public", "no_group", "member", "other", "read", True),
("public", "no_group", "member", "other", "write", False),
("public", "no_group", "member", "other", "unlink", False),
("public", "no_group", "outside", "self", "create", True),
("public", "no_group", "outside", "other", "create", False),
("public", "no_group", "outside", "other", "read", True),
("public", "no_group", "outside", "other", "write", False),
("public", "no_group", "outside", "other", "unlink", False),
("public", "group_matching", "member", "self", "create", False),
("public", "group_matching", "member", "self", "read", True),
("public", "group_matching", "member", "self", "write", True),
("public", "group_matching", "member", "self", "unlink", True),
("public", "group_matching", "member", "other", "create", False),
("public", "group_matching", "member", "other", "read", True),
("public", "group_matching", "member", "other", "write", False),
("public", "group_matching", "member", "other", "unlink", False),
("public", "group_matching", "outside", "self", "create", True),
("public", "group_matching", "outside", "other", "create", False),
("public", "group_matching", "outside", "other", "read", True),
("public", "group_matching", "outside", "other", "write", False),
("public", "group_matching", "outside", "other", "unlink", False),
("public", "group_failing", "member", "self", "create", False),
("public", "group_failing", "member", "self", "read", False),
("public", "group_failing", "member", "self", "write", False),
("public", "group_failing", "member", "self", "unlink", False),
("public", "group_failing", "member", "other", "create", False),
("public", "group_failing", "member", "other", "read", False),
("public", "group_failing", "member", "other", "write", False),
("public", "group_failing", "member", "other", "unlink", False),
("public", "group_failing", "outside", "self", "create", False),
("public", "group_failing", "outside", "other", "create", False),
("public", "group_failing", "outside", "other", "read", False),
("public", "group_failing", "outside", "other", "write", False),
("public", "group_failing", "outside", "other", "unlink", False),
("public", "group", "member", "self", "create", False),
("public", "group", "member", "self", "read", True),
("public", "group", "member", "self", "write", True),
("public", "group", "member", "self", "unlink", True),
("public", "group", "member", "other", "create", False),
("public", "group", "member", "other", "read", True),
("public", "group", "member", "other", "write", False),
("public", "group", "member", "other", "unlink", False),
("public", "group", "outside", "self", "create", False),
("public", "group", "outside", "other", "create", False),
("public", "group", "outside", "other", "read", False),
("public", "group", "outside", "other", "write", False),
("public", "group", "outside", "other", "unlink", False),
("public", "chat", "outside", "self", "create", False),
("public", "chat", "outside", "other", "create", False),
("public", "chat", "outside", "other", "read", False),
("public", "chat", "outside", "other", "write", False),
("public", "chat", "outside", "other", "unlink", False),
("portal", "no_group", "member", "self", "create", False),
("portal", "no_group", "member", "self", "read", True),
("portal", "no_group", "member", "self", "write", True),
("portal", "no_group", "member", "self", "unlink", True),
("portal", "no_group", "member", "other", "create", False),
("portal", "no_group", "member", "other", "read", True),
("portal", "no_group", "member", "other", "write", False),
("portal", "no_group", "member", "other", "unlink", False),
("portal", "no_group", "outside", "self", "create", True),
("portal", "no_group", "outside", "other", "create", False),
("portal", "no_group", "outside", "other", "read", True),
("portal", "no_group", "outside", "other", "write", False),
("portal", "no_group", "outside", "other", "unlink", False),
("portal", "group_matching", "member", "self", "create", False),
("portal", "group_matching", "member", "self", "read", True),
("portal", "group_matching", "member", "self", "write", True),
("portal", "group_matching", "member", "self", "unlink", True),
("portal", "group_matching", "member", "other", "create", False),
("portal", "group_matching", "member", "other", "read", True),
("portal", "group_matching", "member", "other", "write", False),
("portal", "group_matching", "member", "other", "unlink", False),
("portal", "group_matching", "outside", "self", "create", True),
("portal", "group_matching", "outside", "other", "create", False),
("portal", "group_matching", "outside", "other", "read", True),
("portal", "group_matching", "outside", "other", "write", False),
("portal", "group_matching", "outside", "other", "unlink", False),
("portal", "group_failing", "member", "self", "create", False),
("portal", "group_failing", "member", "self", "read", False),
("portal", "group_failing", "member", "self", "write", False),
("portal", "group_failing", "member", "self", "unlink", False),
("portal", "group_failing", "member", "other", "create", False),
("portal", "group_failing", "member", "other", "read", False),
("portal", "group_failing", "member", "other", "write", False),
("portal", "group_failing", "member", "other", "unlink", False),
("portal", "group_failing", "outside", "self", "create", False),
("portal", "group_failing", "outside", "other", "create", False),
("portal", "group_failing", "outside", "other", "read", False),
("portal", "group_failing", "outside", "other", "write", False),
("portal", "group_failing", "outside", "other", "unlink", False),
("portal", "group", "member", "self", "create", False),
("portal", "group", "member", "self", "read", True),
("portal", "group", "member", "self", "write", True),
("portal", "group", "member", "self", "unlink", True),
("portal", "group", "member", "other", "create", False),
("portal", "group", "member", "other", "read", True),
("portal", "group", "member", "other", "write", False),
("portal", "group", "member", "other", "unlink", False),
("portal", "group", "outside", "self", "create", False),
("portal", "group", "outside", "other", "create", False),
("portal", "group", "outside", "other", "read", False),
("portal", "group", "outside", "other", "write", False),
("portal", "group", "outside", "other", "unlink", False),
("portal", "chat", "member", "self", "create", False),
("portal", "chat", "member", "self", "read", True),
("portal", "chat", "member", "self", "write", True),
("portal", "chat", "member", "self", "unlink", True),
("portal", "chat", "member", "other", "create", False),
("portal", "chat", "member", "other", "read", True),
("portal", "chat", "member", "other", "write", False),
("portal", "chat", "member", "other", "unlink", False),
("portal", "chat", "outside", "self", "create", False),
("portal", "chat", "outside", "other", "create", False),
("portal", "chat", "outside", "other", "read", False),
("portal", "chat", "outside", "other", "write", False),
("portal", "chat", "outside", "other", "unlink", False),
("user", "no_group", "member", "self", "create", False),
("user", "no_group", "member", "self", "read", True),
("user", "no_group", "member", "self", "write", True),
("user", "no_group", "member", "self", "unlink", True),
("user", "no_group", "member", "other", "create", True),
("user", "no_group", "member", "other", "read", True),
("user", "no_group", "member", "other", "write", False),
("user", "no_group", "member", "other", "unlink", False),
("user", "no_group", "outside", "self", "create", True),
("user", "no_group", "outside", "other", "create", True),
("user", "no_group", "outside", "other", "read", True),
("user", "no_group", "outside", "other", "write", False),
("user", "no_group", "outside", "other", "unlink", False),
("user", "group_matching", "member", "self", "create", False),
("user", "group_matching", "member", "self", "read", True),
("user", "group_matching", "member", "self", "write", True),
("user", "group_matching", "member", "self", "unlink", True),
("user", "group_matching", "member", "other", "create", True),
("user", "group_matching", "member", "other", "read", True),
("user", "group_matching", "member", "other", "write", False),
("user", "group_matching", "member", "other", "unlink", False),
("user", "group_matching", "outside", "self", "create", True),
("user", "group_matching", "outside", "other", "create", True),
("user", "group_matching", "outside", "other", "read", True),
("user", "group_matching", "outside", "other", "write", False),
("user", "group_matching", "outside", "other", "unlink", False),
("user", "group_failing", "member", "self", "create", False),
("user", "group_failing", "member", "self", "read", False),
("user", "group_failing", "member", "self", "write", False),
("user", "group_failing", "member", "self", "unlink", False),
("user", "group_failing", "member", "other", "create", False),
("user", "group_failing", "member", "other", "read", False),
("user", "group_failing", "member", "other", "write", False),
("user", "group_failing", "member", "other", "unlink", False),
("user", "group_failing", "outside", "self", "create", False),
("user", "group_failing", "outside", "other", "create", False),
("user", "group_failing", "outside", "other", "read", False),
("user", "group_failing", "outside", "other", "write", False),
("user", "group_failing", "outside", "other", "unlink", False),
("user", "group", "member", "self", "create", False),
("user", "group", "member", "self", "read", True),
("user", "group", "member", "self", "write", True),
("user", "group", "member", "self", "unlink", True),
("user", "group", "member", "other", "create", True),
("user", "group", "member", "other", "read", True),
("user", "group", "member", "other", "write", False),
("user", "group", "member", "other", "unlink", False),
("user", "group", "outside", "self", "create", False),
("user", "group", "outside", "other", "create", False),
("user", "group", "outside", "other", "read", False),
("user", "group", "outside", "other", "write", False),
("user", "group", "outside", "other", "unlink", False),
("user", "chat", "member", "self", "create", False),
("user", "chat", "member", "self", "read", True),
("user", "chat", "member", "self", "write", True),
("user", "chat", "member", "self", "unlink", True),
("user", "chat", "member", "other", "create", False),
("user", "chat", "member", "other", "read", True),
("user", "chat", "member", "other", "write", False),
("user", "chat", "member", "other", "unlink", False),
("user", "chat", "outside", "self", "create", False),
("user", "chat", "outside", "other", "create", False),
("user", "chat", "outside", "other", "read", False),
("user", "chat", "outside", "other", "write", False),
("user", "chat", "outside", "other", "unlink", False),
]
for user_key, channel_key, membership, target, operation, result in tests:
channel_id = self._get_channel_id(user_key, channel_key, membership)
if result:
try:
self._execute_action_member(channel_id, user_key, target, operation, result)
except Exception as e: # noqa: BLE001 - re-raising, just with a more contextual message
raise AssertionError(
f"{user_key, channel_key, membership, target, operation} should not raise"
) from e
else:
try:
with self.assertRaises(Exception), mute_logger("odoo.sql_db"), mute_logger(
"odoo.addons.base.models.ir_rule"
):
self._execute_action_member(channel_id, user_key, target, operation, result)
except AssertionError as e:
raise AssertionError(f"{user_key, channel_key, membership, target, operation} should raise") from e
def _get_channel_id(self, user_key, channel_key, membership):
partner = self.env["res.partner"] if user_key == "public" else self.users[user_key].partner_id
guest = self.guest if user_key == "public" else self.env["mail.guest"]
partners = self.other_user.partner_id
if membership == "member":
partners += partner
DiscussChannel = self.env["discuss.channel"].with_user(self.other_user)
if channel_key == "group":
channel = DiscussChannel.create_group(partners.ids)
if membership == "member":
channel.add_members(partner_ids=partner.ids, guest_ids=guest.ids)
elif channel_key == "chat":
channel = DiscussChannel.channel_get(partners.ids)
else:
channel = DiscussChannel.channel_create("Channel", group_id=None)
if membership == "member":
channel.add_members(partner_ids=partner.ids, guest_ids=guest.ids)
if channel_key == "no_group":
channel.group_public_id = None
elif channel_key == "group_matching":
channel.group_public_id = self.secret_group
elif channel_key == "group_failing":
channel.group_public_id = self.env.ref("base.group_system")
return channel.id
def _execute_action_channel(self, user_key, channel_key, membership, operation, result):
current_user = self.users[user_key]
guest = self.guest if user_key == "public" else self.env["mail.guest"]
ChannelAsUser = self.env["discuss.channel"].with_user(current_user).with_context(guest=guest)
if operation == "create":
group_public_id = None
if channel_key == "group_matching":
group_public_id = self.secret_group.id
elif channel_key == "group_failing":
group_public_id = self.env.ref("base.group_system").id
data = {
"name": "Test Channel",
"channel_type": channel_key if channel_key in ("group", "chat") else "channel",
"group_public_id": group_public_id,
}
ChannelAsUser.create(data)
else:
channel = ChannelAsUser.browse(self._get_channel_id(user_key, channel_key, membership))
self.assertEqual(len(channel), 1, "should find the channel")
if operation == "read":
self.assertEqual(len(ChannelAsUser.search([("id", "=", channel.id)])), 1 if result else 0)
channel.read(["name"])
elif operation == "write":
channel.write({"name": "new name"})
elif operation == "unlink":
channel.unlink()
def _execute_action_member(self, channel_id, user_key, target, operation, result):
current_user = self.users[user_key]
partner = self.env["res.partner"] if user_key == "public" else current_user.partner_id
guest = self.guest if user_key == "public" else self.env["mail.guest"]
ChannelMemberAsUser = self.env["discuss.channel.member"].with_user(current_user).with_context(guest=guest)
if operation == "create":
create_data = {"channel_id": channel_id}
if target == "self":
if guest:
create_data["guest_id"] = guest.id
else:
create_data["partner_id"] = partner.id
else:
create_data["partner_id"] = self.other_user_2.partner_id.id
ChannelMemberAsUser.create(create_data)
else:
domain = [("channel_id", "=", channel_id)]
if target == "self":
if guest:
domain.append(("guest_id", "=", guest.id))
else:
domain.append(("partner_id", "=", partner.id))
else:
domain.append(("partner_id", "=", self.other_user.partner_id.id))
member = ChannelMemberAsUser.sudo().search(domain).sudo(False)
self.assertEqual(len(member), 1, "should find the target member")
if operation == "read":
self.assertEqual(len(ChannelMemberAsUser.search(domain)), 1 if result else 0)
member.read(["custom_channel_name"])
elif operation == "write":
member.write({"custom_channel_name": "new name"})
elif operation == "unlink":
member.unlink()
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
from odoo.tests import HttpCase
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.tests.common import HttpCase, tagged
@odoo.tests.tagged('-at_install', 'post_install')
@tagged("post_install", "-at_install")
class TestMailPublicPage(HttpCase):
"""Checks that the invite page redirects to the channel and that all
modules load correctly on the welcome and channel page when authenticated as various users"""
@@ -1,17 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from functools import partial
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common import MailCommon
from odoo.exceptions import AccessError, UserError
discuss_channel_new_test_user = partial(mail_new_test_user, context={'discuss_channel_nosubscribe': False})
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests.common import tagged
class TestDiscussChannelMembers(MailCommon):
@tagged("post_install", "-at_install")
class TestDiscussChannelMember(MailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -25,27 +20,22 @@ class TestDiscussChannelMembers(MailCommon):
'model': cls.secret_group._name,
'res_id': cls.secret_group.id,
})
cls.user_1 = mail_new_test_user(
cls.env, login="user_1", name="User 1", groups="base.group_user,mail.secret_group"
)
cls.user_2 = mail_new_test_user(
cls.env, login="user_2", name="User 2", groups="base.group_user,mail.secret_group"
)
cls.user_3 = mail_new_test_user(
cls.env, login="user_3", name="User 3", groups="base.group_user,mail.secret_group"
)
cls.user_portal = mail_new_test_user(
cls.env, login="user_portal", name="User Portal", groups="base.group_portal"
)
cls.user_public = mail_new_test_user(
cls.env, login="user_public", name="User Public", groups="base.group_public"
)
cls.user_1 = discuss_channel_new_test_user(
cls.env, login='user_1',
name='User 1',
groups='base.group_user,mail.secret_group')
cls.user_2 = discuss_channel_new_test_user(
cls.env, login='user_2',
name='User 2',
groups='base.group_user,mail.secret_group')
cls.user_3 = discuss_channel_new_test_user(
cls.env, login='user_3',
name='User 3',
groups='base.group_user,mail.secret_group')
cls.user_portal = discuss_channel_new_test_user(
cls.env, login='user_portal',
name='User Portal',
groups='base.group_portal')
cls.user_public = discuss_channel_new_test_user(
cls.env, login='user_ublic',
name='User Public',
groups='base.group_public')
cls.group = cls.env['discuss.channel'].create({
'name': 'Group',
@@ -163,8 +153,9 @@ class TestDiscussChannelMembers(MailCommon):
with self.assertRaises(AccessError):
channel_members.with_user(self.user_2).unlink()
# User 3 is in the group, they can kick user 1
channel_members.with_user(self.user_portal).unlink()
# User 3 is in the group, but not admin/owner, they can not kick user 1
with self.assertRaises(AccessError):
channel_members.with_user(self.user_portal).unlink()
# ------------------------------------------------------------
# GROUP BASED CHANNELS
@@ -219,10 +210,8 @@ class TestDiscussChannelMembers(MailCommon):
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.public_channel.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id)
# portal/public users still cannot join a public channel, should go through dedicated controllers
with self.assertRaises(AccessError):
self.public_channel.with_user(self.user_portal).add_members(self.user_portal.partner_id.ids)
with self.assertRaises(AccessError):
self.public_channel.with_user(self.user_portal).add_members(self.user_portal.partner_id.ids)
with self.assertRaises(ValidationError): # public cannot join without having a guest
self.public_channel.with_user(self.user_public).add_members(self.user_public.partner_id.ids)
def test_channel_member_invite_with_guest(self):
+7 -13
View File
@@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tests.common import tagged, users
from odoo.tools import mute_logger
@tagged('RTC')
class TestChannelInternals(MailCommon):
@tagged("RTC", "post_install", "-at_install")
class TestChannelRTC(MailCommon):
@users('employee')
@mute_logger('odoo.models.unlink')
@@ -108,8 +106,7 @@ class TestChannelInternals(MailCommon):
[
(self.cr.dbname, 'discuss.channel', channel.id), # update new session
(self.cr.dbname, 'discuss.channel', channel.id), # message_post "started a live conference" (not asserted below)
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below)
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below)
(self.cr.dbname, 'discuss.channel', channel.id), # update of last interest (not asserted below)
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation
(self.cr.dbname, 'discuss.channel', channel.id), # update list of invitations
],
@@ -190,8 +187,7 @@ class TestChannelInternals(MailCommon):
[
(self.cr.dbname, 'discuss.channel', channel.id), # update new session
(self.cr.dbname, 'discuss.channel', channel.id), # message_post "started a live conference" (not asserted below)
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below)
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below)
(self.cr.dbname, 'discuss.channel', channel.id), # update of last interest (not asserted below)
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation
(self.cr.dbname, 'mail.guest', test_guest.id), # incoming invitation
(self.cr.dbname, 'discuss.channel', channel.id), # update list of invitations
@@ -646,14 +642,12 @@ class TestChannelInternals(MailCommon):
[
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # channel joined (not asserted below)
(self.cr.dbname, 'discuss.channel', channel.id), # message_post "invited" (not asserted below)
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below)
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below)
(self.cr.dbname, 'discuss.channel', channel.id), # update of last interest (not asserted below)
(self.cr.dbname, 'discuss.channel', channel.id), # new members (not asserted below)
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation
(self.cr.dbname, 'mail.guest', test_guest.id), # incoming invitation
(self.cr.dbname, 'discuss.channel', channel.id), # update list of invitations
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below)
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below)
(self.cr.dbname, 'discuss.channel', channel.id), # update of last interest (not asserted below)
(self.cr.dbname, 'discuss.channel', channel.id), # new member (guest) (not asserted below)
(self.cr.dbname, 'mail.guest', test_guest.id), # channel joined for guest (not asserted below)
],
@@ -3,7 +3,7 @@
from requests.exceptions import HTTPError
from odoo import Command, http
from odoo.tests import tagged, HttpCase
from odoo.tests.common import tagged, HttpCase
from odoo.tools import file_open, mute_logger
@@ -11,7 +11,7 @@ from odoo.tools import file_open, mute_logger
class TestToggleUpload(HttpCase):
def test_upload_allowed(self):
self.authenticate(None, None)
channel = self.env["discuss.channel"].create({"name": "General"})
channel = self.env["discuss.channel"].create({"name": "General", "group_public_id": None})
guest = self.env["mail.guest"].create({"name": "Guest"})
channel.write({"channel_member_ids": [Command.create({"guest_id": guest.id})]})
self.assertFalse(channel.allow_public_upload)
@@ -31,7 +31,7 @@ class TestToggleUpload(HttpCase):
def test_upload_denied(self):
self.authenticate(None, None)
channel = self.env["discuss.channel"].create({"name": "General"})
channel = self.env["discuss.channel"].create({"name": "General", "group_public_id": None})
guest = self.env["mail.guest"].create({"name": "Guest"})
channel.write({"channel_member_ids": [Command.create({"guest_id": guest.id})]})
self.assertFalse(channel.allow_public_upload)
+2 -3
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from markupsafe import Markup
@@ -9,10 +8,10 @@ import requests
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.mail.tools import link_preview
from odoo.tests import tagged
from odoo.tests.common import tagged
@tagged('mail_link_preview', 'mail_message')
@tagged("mail_link_preview", "mail_message", "post_install", "-at_install")
class TestLinkPreview(MailCommon):
@classmethod
+2 -2
View File
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests.common import users
from odoo.tests.common import tagged, users
@tagged("post_install", "-at_install")
class TestResUsersSettings(MailCommon):
@users('employee')
+1 -2
View File
@@ -36,8 +36,7 @@ class Users(models.Model):
def _init_odoobot(self):
self.ensure_one()
odoobot_id = self.env['ir.model.data']._xmlid_to_res_id("base.partner_root")
channel_info = self.env['discuss.channel'].channel_get([odoobot_id, self.partner_id.id])
channel = self.env['discuss.channel'].browse(channel_info['id'])
channel = self.env['discuss.channel'].channel_get([odoobot_id, self.partner_id.id])
message = Markup("%s<br/>%s<br/><b>%s</b> <span class=\"o_odoobot_command\">:)</span>") % (
_("Hello,"),
_("Odoo's chat helps employees collaborate efficiently. I'm here to help you discover its features."),
+3
View File
@@ -12,7 +12,10 @@
'calendar',
'crm',
'crm_livechat',
'hr_attendance',
'hr_fleet',
'hr_holidays',
'hr_homeworking',
'im_livechat',
'mail',
'mail_bot',
@@ -12,7 +12,7 @@ from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
@tagged('post_install', '-at_install')
class TestDiscussFullPerformance(HttpCase):
_query_count = 73
_query_count = 62
def setUp(self):
super().setUp()
@@ -1157,6 +1157,7 @@ class TestMailgateway(MailCommon):
test_channel = self.env['discuss.channel'].create({
'name': 'Test',
'channel_partner_ids': [(4, self.partner_1.id)],
'group_public_id': None,
})
self.fake_email.write({
'model': 'discuss.channel',
+3 -4
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
@@ -8,11 +7,11 @@ from unittest.mock import patch
from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon
from odoo.addons.test_mail.models.test_mail_models import MailTestSimple
from odoo.exceptions import AccessError, UserError
from odoo.tests.common import tagged, users
from odoo.tools import is_html_empty, mute_logger, formataddr
from odoo.tests import tagged, users
@tagged('mail_message')
@tagged("mail_message", "post_install", "-at_install")
class TestMessageValues(MailCommon):
@classmethod
@@ -321,7 +320,7 @@ class TestMessageValues(MailCommon):
self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id.split('@')[0])
@tagged('mail_message')
@tagged("mail_message", "post_install", "-at_install")
class TestMessageAccess(MailCommon):
@classmethod
@@ -79,5 +79,6 @@ class DiscussChannel(models.Model):
message_author_id = message.author_id
visitor = self.livechat_visitor_id
if len(self) == 1 and visitor and message_author_id != self.livechat_operator_id:
visitor._update_visitor_last_visit()
# sudo: website.visitor: updating data of a specific visitor
visitor.sudo()._update_visitor_last_visit()
return message
@@ -56,6 +56,7 @@ class Website(models.Model):
chat_request_channel.write({'channel_member_ids': [Command.unlink(channel_guest_member.id), Command.create({'guest_id': current_guest.id})]})
if not current_guest:
channel_guest_member.guest_id._set_auth_cookie()
chat_request_channel = chat_request_channel.with_context(guest=channel_guest_member.guest_id.sudo(False))
return {
"folded": False,
"id": chat_request_channel.id,
@@ -82,7 +82,8 @@ class WebsiteVisitor(models.Model):
discuss_channels = self.env['discuss.channel'].create(discuss_channel_vals_list)
for channel in discuss_channels:
if not channel.livechat_visitor_id.partner_id:
self.env['mail.guest']._find_or_create_for_channel(
# sudo: mail.guest - creating a guest and their member in a dedicated channel created from livechat
self.env['mail.guest'].sudo()._find_or_create_for_channel(
channel,
country_id=country.id,
name=_("Visitor #%d", channel.livechat_visitor_id.id),