diff options
| author | jem-odoo <jem@openerp.com> | 2019-01-31 08:43:57 +0000 |
|---|---|---|
| committer | jem-odoo <jem@openerp.com> | 2019-02-06 09:55:13 +0000 |
| commit | 17ac041bfa2d92a11339e63016bce4696362b333 (patch) | |
| tree | cdf5fd4273de8b04e6ad7906ef079c4e6d242913 | |
| parent | 4cb228d823e0346e6052091d397249b8e39d576b (diff) | |
[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
| -rw-r--r-- | addons/portal/controllers/mail.py | 42 | ||||
| -rw-r--r-- | addons/portal/models/mail_thread.py | 24 | ||||
| -rw-r--r-- | addons/portal/models/portal_mixin.py | 20 | ||||
| -rw-r--r-- | addons/portal/static/src/js/portal_chatter.js | 5 | ||||
| -rw-r--r-- | addons/portal/views/portal_templates.xml | 2 | ||||
| -rw-r--r-- | addons/sale/controllers/portal.py | 8 |
6 files changed, 62 insertions, 39 deletions
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): @@ -134,22 +132,6 @@ class PortalMixin(models.AbstractModel): 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): """ Get a portal url for this model, including access_token. 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 --> <template id="message_thread"> <div id="discussion" class="d-print-none o_portal_chatter o_not_editable p-0" diff --git a/addons/sale/controllers/portal.py b/addons/sale/controllers/portal.py index 467a03764da..702aa08dc56 100644 --- a/addons/sale/controllers/portal.py +++ b/addons/sale/controllers/portal.py @@ -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" |
