[REF] portal: posting message with signed token
Before this commit, it was possible to post a message on a document the user has no access, thanks to a token. In addition to this token, if a hash (signed token with a partner id), the author_id of the message was forced. This commit change a little bit that logic by distinguish 2 cases: 1/ Token only: anyone with the token can post a message on the document. If the user is not logged (public with token), the author_id will be the customer of the document (or the public user). This case is moslty used for business document, through the portal or with the "share link" for instance. 2/ Signed token: user has no write access to the document, but can post a message on it. The user have to be logged, as the token is signed with its identifier. The goal here is to avoid leaking the access token's document to all visitors. This case is mostly used for public content, such as blog posts, slides, ... The token case was already existing. The second is now working with this commit. To do so, it was required to - move `_sign_token` from the portal mixin to mail.thread. - transfering 'pid' and 'hash' parameters from the controller to template to js widgets. This commit also change the `_message_post_helper` signature by making the 3 first parameters required, as to post a message you need at lease res_model, res_id and the message body. The optional argurments and kwargs are here to check the bypassing access rights mecanism. Task-1902304
This commit is contained in:
@@ -19,43 +19,59 @@ def _has_token_access(res_model, res_id, token=''):
|
||||
return (token and record and consteq(record[token_field], token))
|
||||
|
||||
|
||||
def _message_post_helper(res_model='', res_id=None, message='', token='', nosubscribe=True, **kw):
|
||||
""" Generic chatter function, allowing to write on *any* object that inherits mail.thread.
|
||||
If a token is specified, all logged in users will be able to write a message regardless
|
||||
of access rights; if the user is the public user, the message will be posted under the name
|
||||
of the partner_id of the object (or the public user if there is no partner_id on the object).
|
||||
def _message_post_helper(res_model, res_id, message, token='', nosubscribe=True, **kw):
|
||||
""" Generic chatter function, allowing to write on *any* object that inherits mail.thread. We
|
||||
distinguish 2 cases:
|
||||
1/ If a token is specified, all logged in users will be able to write a message regardless
|
||||
of access rights; if the user is the public user, the message will be posted under the name
|
||||
of the partner_id of the object (or the public user if there is no partner_id on the object).
|
||||
|
||||
2/ If a signed token is specified (`hash`) and also a partner_id (`pid`), all post message will
|
||||
be done under the name of the partner_id (as it is signed). This should be used to avoid leaking
|
||||
token to all users.
|
||||
|
||||
Required parameters
|
||||
:param string res_model: model name of the object
|
||||
:param int res_id: id of the object
|
||||
:param string message: content of the message
|
||||
|
||||
optional keywords arguments:
|
||||
Optional keywords arguments:
|
||||
:param string token: access token if the object's model uses some kind of public access
|
||||
using tokens (usually a uuid4) to bypass access rules
|
||||
:param string hash: signed token by a partner if model uses some token field to bypass access right
|
||||
post messages.
|
||||
:param string pid: identifier of the res.partner used to sign the hash
|
||||
:param bool nosubscribe: set False if you want the partner to be set as follower of the object when posting (default to True)
|
||||
|
||||
The rest of the kwargs are passed on to message_post()
|
||||
"""
|
||||
record = request.env[res_model].browse(res_id)
|
||||
author_id = request.env.user.partner_id.id if request.env.user.partner_id else False
|
||||
# Token Case: token required to post message as document customer (if not logged) or as itself even if user has not the access
|
||||
if token:
|
||||
access_as_sudo = _has_token_access(res_model, res_id, token=token)
|
||||
if access_as_sudo:
|
||||
record = record.sudo()
|
||||
if request.env.user._is_public():
|
||||
if kw.get('pid') and consteq(kw.get('hash'), record._sign_token(int(kw.get('pid')))):
|
||||
author_id = kw.get('pid')
|
||||
else:
|
||||
# TODO : After adding the pid and sign_token in access_url when send invoice by email, remove this line
|
||||
# TODO : Author must be Public User (to rename to 'Anonymous')
|
||||
author_id = record.partner_id.id if hasattr(record, 'partner_id') and record.partner_id.id else author_id
|
||||
# TODO : After adding the pid and sign_token in access_url when send invoice by email, remove this line
|
||||
# TODO : Author must be Public User (to rename to 'Anonymous')
|
||||
author_id = record.partner_id.id if hasattr(record, 'partner_id') and record.partner_id.id else author_id
|
||||
else:
|
||||
if not author_id:
|
||||
raise NotFound()
|
||||
else:
|
||||
raise Forbidden()
|
||||
# Signed Token Case: hash implies token is signed by partner. So author_id is forced
|
||||
elif kw.get('hash') and kw.get('pid'):
|
||||
if consteq(kw['hash'], record._sign_token(int(kw['pid']))):
|
||||
record = record.sudo()
|
||||
author_id = kw.get('pid')
|
||||
else:
|
||||
raise Forbidden()
|
||||
kw.pop('csrf_token', None)
|
||||
kw.pop('attachment_ids', None)
|
||||
kw.pop('hash', None)
|
||||
kw.pop('pid', None)
|
||||
return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(body=message,
|
||||
message_type=kw.pop('message_type', "comment"),
|
||||
subtype=kw.pop('subtype', "mt_comment"),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
@@ -12,3 +15,22 @@ class MailThread(models.AbstractModel):
|
||||
website_message_ids = fields.One2many('mail.message', 'res_id', string='Website Messages',
|
||||
domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment')], auto_join=True,
|
||||
help="Website communication history")
|
||||
|
||||
@api.multi
|
||||
def _sign_token(self, pid):
|
||||
"""Generate a secure hash for this record with the email of the recipient with whom the record have been shared.
|
||||
|
||||
This is used to determine who is opening the link
|
||||
to be able for the recipient to post messages on the document's portal view.
|
||||
|
||||
:param str email:
|
||||
Email of the recipient that opened the link.
|
||||
"""
|
||||
self.ensure_one()
|
||||
# check token field exists
|
||||
if self._mail_post_token_field not in self._fields:
|
||||
raise NotImplementedError(_("Model %s does not support token signature, as it does not have %s field.") % (self._name, self._mail_post_token_field))
|
||||
# sign token
|
||||
secret = self.env["ir.config_parameter"].sudo().get_param("database.secret")
|
||||
token = (self.env.cr.dbname, self[self._mail_post_token_field], pid)
|
||||
return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import uuid
|
||||
import hashlib
|
||||
import hmac
|
||||
from werkzeug.urls import url_encode
|
||||
from odoo import api, exceptions, fields, models, tools, _
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
|
||||
|
||||
class PortalMixin(models.AbstractModel):
|
||||
@@ -133,22 +131,6 @@ class PortalMixin(models.AbstractModel):
|
||||
'active_model': self.env.context['active_model']}
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def _sign_token(self, pid):
|
||||
"""Generate a secure hash for this record with the email of the recipient with whom the record have been shared.
|
||||
|
||||
This is used to determine who is opening the link
|
||||
to be able for the recipient to post messages on the document's portal view.
|
||||
|
||||
:param str email:
|
||||
Email of the recipient that opened the link.
|
||||
"""
|
||||
self.ensure_one()
|
||||
secret = self.env["ir.config_parameter"].sudo().get_param(
|
||||
"database.secret")
|
||||
token = (self.env.cr.dbname, self.access_token, pid)
|
||||
return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
|
||||
@api.multi
|
||||
def get_portal_url(self, suffix=None, report_type=None, download=None, query_string=None, anchor=None):
|
||||
"""
|
||||
|
||||
@@ -83,8 +83,11 @@ var PortalChatter = Widget.extend({
|
||||
'pager_start': 1,
|
||||
'is_user_public': true,
|
||||
'is_user_publisher': false,
|
||||
'hash': false,
|
||||
'pid': false,
|
||||
'domain': [],
|
||||
});
|
||||
|
||||
this.set('messages', []);
|
||||
this.set('message_count', this.options['message_count']);
|
||||
this.set('pager', {});
|
||||
@@ -95,11 +98,11 @@ var PortalChatter = Widget.extend({
|
||||
var self = this;
|
||||
// load qweb template and init data
|
||||
return $.when(
|
||||
this._super.apply(this, arguments),
|
||||
rpc.query({
|
||||
route: '/mail/chatter_init',
|
||||
params: this._messageFetchPrepareParams()
|
||||
}),
|
||||
this._super.apply(this, arguments),
|
||||
).then(function(result){
|
||||
self.result = result;
|
||||
self.options = _.extend(self.options, self.result['options'] || {});
|
||||
|
||||
@@ -473,6 +473,8 @@
|
||||
:token string (optional): if you want your chatter to be available for non-logged user,
|
||||
you can use a token to verify the identity of the user;
|
||||
the message will be posted with the identity of the partner_id of the object
|
||||
:hash : signed token with the partner_id using `_sign_token` method (on mail.thread)
|
||||
:pid : identifier of the partner signing the token
|
||||
-->
|
||||
<template id="message_thread">
|
||||
<div id="discussion" class="d-print-none o_portal_chatter o_not_editable p-0"
|
||||
|
||||
@@ -159,7 +159,7 @@ class CustomerPortal(CustomerPortal):
|
||||
if order_sudo and request.session.get('view_quote_%s' % order_sudo.id) != now and request.env.user.share and access_token:
|
||||
request.session['view_quote_%s' % order_sudo.id] = now
|
||||
body = _('Quotation viewed by customer')
|
||||
_message_post_helper(res_model='sale.order', res_id=order_sudo.id, message=body, token=order_sudo.access_token, message_type='notification', subtype="mail.mt_note", partner_ids=order_sudo.user_id.sudo().partner_id.ids)
|
||||
_message_post_helper('sale.order', order_sudo.id, body, token=order_sudo.access_token, message_type='notification', subtype="mail.mt_note", partner_ids=order_sudo.user_id.sudo().partner_id.ids)
|
||||
|
||||
values = {
|
||||
'sale_order': order_sudo,
|
||||
@@ -223,9 +223,7 @@ class CustomerPortal(CustomerPortal):
|
||||
pdf = request.env.ref('sale.action_report_saleorder').sudo().render_qweb_pdf([order_sudo.id])[0]
|
||||
|
||||
_message_post_helper(
|
||||
res_model='sale.order',
|
||||
res_id=order_sudo.id,
|
||||
message=_('Order signed by %s') % (name,),
|
||||
'sale.order', order_sudo.id, _('Order signed by %s') % (name,),
|
||||
attachments=[('%s.pdf' % order_sudo.name, pdf)],
|
||||
**({'token': access_token} if access_token else {}))
|
||||
|
||||
@@ -249,7 +247,7 @@ class CustomerPortal(CustomerPortal):
|
||||
query_string = False
|
||||
if order_sudo.has_to_be_signed() and message:
|
||||
order_sudo.action_cancel()
|
||||
_message_post_helper(message=message, res_id=order_id, res_model='sale.order', **{'token': access_token} if access_token else {})
|
||||
_message_post_helper('sale.order', order_id, message, **{'token': access_token} if access_token else {})
|
||||
else:
|
||||
query_string = "&message=cant_reject"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user