diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py index dcccbb61fa1..49719c7568b 100644 --- a/addons/portal/controllers/mail.py +++ b/addons/portal/controllers/mail.py @@ -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"), diff --git a/addons/portal/models/mail_thread.py b/addons/portal/models/mail_thread.py index 947839bc093..aea29ea3ec7 100644 --- a/addons/portal/models/mail_thread.py +++ b/addons/portal/models/mail_thread.py @@ -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() diff --git a/addons/portal/models/portal_mixin.py b/addons/portal/models/portal_mixin.py index 99d8f18a266..71d8f970ac2 100644 --- a/addons/portal/models/portal_mixin.py +++ b/addons/portal/models/portal_mixin.py @@ -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): """ diff --git a/addons/portal/static/src/js/portal_chatter.js b/addons/portal/static/src/js/portal_chatter.js index 5a56a35bed9..bfd8fe95957 100644 --- a/addons/portal/static/src/js/portal_chatter.js +++ b/addons/portal/static/src/js/portal_chatter.js @@ -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'] || {}); diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 7f4f78b63c4..462a93a5936 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -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 -->