From ea7bddcc3e99f6514efef90fcb26e5ce6f7e427e Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Mon, 25 Jun 2018 16:55:20 +0200 Subject: [PATCH 1/5] [FIX] event, event_sale, website_event: allow portal access to own registrations OPW 1859364 --- addons/event/security/event_security.xml | 7 +++++++ addons/event/security/ir.model.access.csv | 3 ++- addons/event_sale/__openerp__.py | 1 + addons/event_sale/security/event_security.xml | 8 ++++++++ addons/website_event/views/website_event.xml | 2 +- 5 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 addons/event_sale/security/event_security.xml diff --git a/addons/event/security/event_security.xml b/addons/event/security/event_security.xml index 44a9a1b9c8a..d6874af2604 100644 --- a/addons/event/security/event_security.xml +++ b/addons/event/security/event_security.xml @@ -55,6 +55,13 @@ ] + + Event/Registration: Portal + + + ['|', ('email', '=', user.partner_id.email), ('partner_id', '=', user.partner_id.id)] + + diff --git a/addons/event/security/ir.model.access.csv b/addons/event/security/ir.model.access.csv index 35f82422352..004097fa3b7 100644 --- a/addons/event/security/ir.model.access.csv +++ b/addons/event/security/ir.model.access.csv @@ -5,7 +5,8 @@ access_event_event,event.event,model_event_event,event.group_event_user,1,1,1,1 access_event_registration,event.registration,model_event_registration,event.group_event_user,1,1,1,1 access_report_event_registration,report.event.registration,model_report_event_registration,event.group_event_user,1,1,1,1 access_event_event_portal,event.event,model_event_event,,1,0,0,0 -access_event_registration_portal,event.registration,model_event_registration,,0,0,0,0 +access_event_registration_portal,event.registration,model_event_registration,base.group_portal,1,0,0,0 +access_event_registration_employee,event.registration,model_event_registration,base.group_user,1,0,0,0 access_event_mail,event.mail,model_event_mail,event.group_event_user,1,0,0,0 access_event_mail_manager,event.mail manager,model_event_mail,event.group_event_manager,1,1,1,1 access_event_mail_registration,event.mail.registration,model_event_mail_registration,event.group_event_user,1,0,0,0 diff --git a/addons/event_sale/__openerp__.py b/addons/event_sale/__openerp__.py index dae8a0be869..c5e06ae8942 100644 --- a/addons/event_sale/__openerp__.py +++ b/addons/event_sale/__openerp__.py @@ -26,6 +26,7 @@ this event. 'event_sale_data.xml', 'report/event_event_templates.xml', 'security/ir.model.access.csv', + 'security/event_security.xml', 'wizard/event_edit_registration.xml', ], 'demo': ['event_demo.xml'], diff --git a/addons/event_sale/security/event_security.xml b/addons/event_sale/security/event_security.xml new file mode 100644 index 00000000000..8c16690513d --- /dev/null +++ b/addons/event_sale/security/event_security.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/addons/website_event/views/website_event.xml b/addons/website_event/views/website_event.xml index 7ebcb5900df..637685643ae 100644 --- a/addons/website_event/views/website_event.xml +++ b/addons/website_event/views/website_event.xml @@ -431,7 +431,7 @@ class="btn btn-primary btn-lg pull-right a-submit" t-attf-id="#{event.id}">Register Now
Event registration not yet started. - + Configure and Launch Event Registration
From ef444da57a0e5b3a8426a6fac8cef3f6d856b148 Mon Sep 17 00:00:00 2001 From: Ruchir Shukla Date: Tue, 16 Jan 2018 15:23:08 +0530 Subject: [PATCH 2/5] [FIX] stock: do_new_transfer callable in xml-rpc When returning None, the XML-RPC can trigger an error, making the api unusable in certain cases. So, we added return True and if the context is None we use an empty dict, so if the context is returned in a dict, it is not returning None either. Tests were adapted too. Closes #22264 --- addons/sale_stock/tests/test_sale_stock.py | 4 ++-- addons/stock/stock.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py index 6085c639f6e..a19cdcd268f 100644 --- a/addons/sale_stock/tests/test_sale_stock.py +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -51,7 +51,7 @@ class TestSaleStock(TestSale): pick_2 = self.so.picking_ids[0] pick_2.force_assign() pick_2.pack_operation_product_ids.write({'qty_done': 1}) - self.assertIsNone(pick_2.do_new_transfer(), 'Sale Stock: second picking should be final without need for a backorder') + self.assertTrue(pick_2.do_new_transfer(), 'Sale Stock: second picking should be final without need for a backorder') self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" after complete delivery') del_qties = [sol.qty_delivered for sol in self.so.order_line] del_qties_truth = [2.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line] @@ -98,7 +98,7 @@ class TestSaleStock(TestSale): pick = self.so.picking_ids pick.force_assign() pick.pack_operation_product_ids.write({'qty_done': 2}) - self.assertIsNone(pick.do_new_transfer(), 'Sale Stock: complete delivery should not need a backorder') + self.assertTrue(pick.do_new_transfer(), 'Sale Stock: complete delivery should not need a backorder') del_qties = [sol.qty_delivered for sol in self.so.order_line] del_qties_truth = [2.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line] self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after partial delivery') diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 45fe1e380d9..06bafd6a341 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1589,6 +1589,8 @@ class stock_picking(models.Model): def do_new_transfer(self, cr, uid, ids, context=None): pack_op_obj = self.pool['stock.pack.operation'] data_obj = self.pool['ir.model.data'] + if not context: + context = {} for pick in self.browse(cr, uid, ids, context=context): to_delete = [] if not pick.move_lines and not pick.pack_operation_ids: @@ -1642,7 +1644,7 @@ class stock_picking(models.Model): if to_delete: pack_op_obj.unlink(cr, uid, to_delete, context=context) self.do_transfer(cr, uid, ids, context=context) - return + return True def check_backorder(self, cr, uid, picking, context=None): need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(cr, uid, picking, done_qtys=True, context=context) From c1b6cfaab8d42885533e23985058cfc78b48d7c7 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 5 Jun 2018 15:34:41 +0200 Subject: [PATCH 3/5] [FIX] google_account: backport of c444b5a293 In this commit, our hero backport c444b5a293 to 9.0 [FIX] google_account: fix google request exception management Error thrown by google request is an urllib2.HTTPError that can be read and loaded in JSON. However in some cases the result of the read may be void or not JSON-ready. This was causing a crash in the error management and hid the actual issue. This commit tries to read and JSON-load the error but fall back on simply displaying the raw error in case of issue when handling it. opw-1851612 --- addons/google_account/google_account.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/google_account/google_account.py b/addons/google_account/google_account.py index 47b67fbc28b..4eff3c2706f 100644 --- a/addons/google_account/google_account.py +++ b/addons/google_account/google_account.py @@ -127,7 +127,11 @@ class google_service(osv.osv_memory): registry = openerp.modules.registry.RegistryManager.get(request.session.db) with registry.cursor() as cur: self.pool['res.users'].write(cur, uid, [uid], {'google_%s_rtoken' % service: False}, context=context) - error_key = json.loads(e.read()).get("error", "nc") + try: + error_file = e.read() + error_key = json.loads(error_file).get("error", "nc") + except: + error_key = e _logger.exception("Bad google request : %s !" % error_key) error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired [%s]") % error_key raise self.pool.get('res.config.settings').get_config_warning(cr, error_msg, context=context) From 29c00a56daa377e40f4d28e86a10f105617fd243 Mon Sep 17 00:00:00 2001 From: Toufik Benjaa Date: Tue, 20 Mar 2018 16:36:26 +0100 Subject: [PATCH 4/5] [IMP] http: Sessions implicit deactivation - Store a token inside sessions to allow implicit session deactivation when needed. backport of @da1f153d61d747d9357694382fe04f96c0ca886a @c8243e71c6da37547a19f61c58f25d5d03e13d38 --- addons/auth_crypt/auth_crypt.py | 5 ++++ addons/auth_oauth/res_users.py | 5 ++++ addons/web/controllers/main.py | 10 ++----- openerp/addons/base/res/res_users.py | 33 +++++++++++++++++++++ openerp/http.py | 44 ++++++++++++++++++---------- openerp/service/security.py | 11 +++++++ openerp/tests/common.py | 3 +- 7 files changed, 88 insertions(+), 23 deletions(-) diff --git a/addons/auth_crypt/auth_crypt.py b/addons/auth_crypt/auth_crypt.py index 7c9d0c5fa2c..c8ac4876a55 100644 --- a/addons/auth_crypt/auth_crypt.py +++ b/addons/auth_crypt/auth_crypt.py @@ -3,6 +3,7 @@ import logging from passlib.context import CryptContext import openerp +from openerp import api from openerp.osv import fields, osv from openerp.addons.base.res import res_users @@ -95,3 +96,7 @@ class res_users(osv.osv): internally """ return default_crypt_context + + @api.model + def _get_session_token_fields(self): + return super(res_users, self)._get_session_token_fields() | {'password_crypt'} diff --git a/addons/auth_oauth/res_users.py b/addons/auth_oauth/res_users.py index 60fdd7319a1..f89234a743e 100644 --- a/addons/auth_oauth/res_users.py +++ b/addons/auth_oauth/res_users.py @@ -6,6 +6,7 @@ import urllib2 import json import openerp +from openerp import api from openerp.addons.auth_signup.res_users import SignupError from openerp.osv import osv, fields from openerp import SUPERUSER_ID @@ -125,4 +126,8 @@ class res_users(osv.Model): if not res: raise + @api.model + def _get_session_token_fields(self): + return super(res_users, self)._get_session_token_fields() | {'oauth_access_token'} + # diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index a1ead7e5d1a..158ae695268 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -37,6 +37,7 @@ from openerp.tools.misc import str2bool, xlwt from openerp import http from openerp.http import request, serialize_exception as _serialize_exception, content_disposition from openerp.exceptions import AccessError, UserError +from openerp.service.report import exp_report, exp_report_get _logger = logging.getLogger(__name__) @@ -1479,7 +1480,6 @@ class Reports(http.Controller): def index(self, action, token): action = json.loads(action) - report_srv = request.session.proxy("report") context = dict(request.context) context.update(action["context"]) @@ -1492,15 +1492,11 @@ class Reports(http.Controller): report_ids = action['datas'].pop('ids') report_data.update(action['datas']) - report_id = report_srv.report( - request.session.db, request.session.uid, request.session.password, - action["report_name"], report_ids, - report_data, context) + report_id = exp_report(request.session.db, request.session.uid, action["report_name"], report_ids, report_data, context) report_struct = None while True: - report_struct = report_srv.report_get( - request.session.db, request.session.uid, request.session.password, report_id) + report_struct = exp_report_get(request.session.db, request.session.uid, report_id) if report_struct["state"]: break diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index 2c5bce22725..d573b9d4658 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -2,11 +2,13 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import itertools import logging +import hmac from functools import partial from itertools import repeat from lxml import etree from lxml.builder import E +from hashlib import sha256 import openerp from openerp import api @@ -378,6 +380,8 @@ class res_users(osv.osv): for id in ids: if id in self.__uid_cache[db]: del self.__uid_cache[db][id] + if any(key in values for key in self._get_session_token_fields(cr, uid)): + self._invalidate_session_cache(cr, uid) self.context_get.clear_cache(self) self.has_group.clear_cache(self) return res @@ -511,6 +515,35 @@ class res_users(osv.osv): finally: cr.close() + @api.model + def _get_session_token_fields(self): + return {'id', 'login', 'password', 'active'} + + @tools.ormcache('sid') + def _compute_session_token(self, sid): + """ Compute a session token given a session id and a user id """ + # retrieve the fields used to generate the session token + session_fields = ', '.join(sorted(self._get_session_token_fields())) + self.env.cr.execute("""SELECT %s, (SELECT value FROM ir_config_parameter WHERE key='database.secret') + FROM res_users + WHERE id=%%s""" % (session_fields), (self.id,)) + if self.env.cr.rowcount != 1: + self._invalidate_session_cache() + return False + data_fields = self.env.cr.fetchone() + # generate hmac key + key = (u'%s' % (data_fields,)).encode('utf-8') + # hmac the session id + data = sid.encode('utf-8') + h = hmac.new(key, data, sha256) + # keep in the cache the token + return h.hexdigest() + + @api.model + def _invalidate_session_cache(self): + """ Clear the session cache """ + self._compute_session_token.clear_cache(self) + def change_password(self, cr, uid, old_passwd, new_passwd, context=None): """Change current user password. Old password must be provided explicitly to prevent hijacking an existing user session, or for cases where the cleartext diff --git a/openerp/http.py b/openerp/http.py index 05778f0d290..22a9ce5ccf3 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -313,7 +313,15 @@ class WebRequest(object): # case, the request cursor is unusable. Rollback transaction to create a new one. if self._cr: self._cr.rollback() - self.env.clear() + # With the session patch, we now clear the environment only if it exists + # We do so by checking if the environment is stored in request.__dict__ + # Which is how lazy_property works. + # We do this, to avoid creating the environment before it is needed. + # For example some auth='none' controllers do "request.uid = request.session.uid" then + # "request.env.user ..." which is broken if we create the environment by doing self.env.clear() + # since it will not be linked to a user. + if self.__dict__.get('env'): + self.env.clear() result = self.endpoint(*a, **kw) if isinstance(result, Response) and result.is_qweb: # Early rendering of lazy responses to benefit from @service_model.check protection @@ -1123,7 +1131,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.db = db self.uid = uid self.login = login - self.password = password + self.session_token = uid and security.compute_session_token(self, request.env) request.uid = uid request.disable_db = False @@ -1138,7 +1146,20 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): """ if not self.db or not self.uid: raise SessionExpiredException("Session expired") - security.check(self.db, self.uid, self.password) + # We create our own environment instead of the request's one. + # This is due to the fact that the member "uid" on the request object isn't set yet. + # If we try to use the request's environment, the session checking will never succeed since + # the environment isn't bound to any user and it needs to be. + env = openerp.api.Environment(request.cr, self.uid, self.context) + # == BACKWARD COMPATIBILITY TO CONVERT OLD SESSION TYPE TO THE NEW ONES ! REMOVE ME AFTER 11.0 == + if self.get('password'): + security.check(self.db, self.uid, self.password) + self.session_token = security.compute_session_token(self, env) + self.pop('password') + # ================================================================================================= + # here we check if the session is still valid + if not security.check_session(self, env): + raise SessionExpiredException("Session expired") def logout(self, keep_db=False): for k in self.keys(): @@ -1151,7 +1172,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.setdefault("db", None) self.setdefault("uid", None) self.setdefault("login", None) - self.setdefault("password", None) + self.setdefault("session_token", None) self.setdefault("context", {}) def get_context(self): @@ -1211,12 +1232,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): @_login.setter def _login(self, value): self.login = value - @property - def _password(self): - return self.password - @_password.setter - def _password(self, value): - self.password = value def send(self, service_name, method, *args): """ @@ -1239,11 +1254,10 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): Ensures this session is valid (logged into the openerp server) """ - if self.uid and not force: + if self.uid and self.session_token and not force: return - # TODO use authenticate instead of login - self.uid = self.proxy("common").login(self.db, self.login, self.password) - if not self.uid: + + if not self.uid or not security.check_session(self, request.env): raise AuthenticationError("Authentication failure") def ensure_valid(self): @@ -1272,7 +1286,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): Use the registry and cursor in :data:`request` instead. """ self.assert_valid() - r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id) + r = service_model.exec_workflow(self.db, self.uid, model, signal, id) return r def model(self, model): diff --git a/openerp/service/security.py b/openerp/service/security.py index 8ab38d3d659..28c85788a0a 100644 --- a/openerp/service/security.py +++ b/openerp/service/security.py @@ -11,3 +11,14 @@ def login(db, login, password): def check(db, uid, passwd): res_users = openerp.registry(db)['res.users'] return res_users.check(db, uid, passwd) + +def compute_session_token(session, env): + self = env['res.users'].browse(session.uid) + return self._compute_session_token(session.sid) + +def check_session(session, env): + self = env['res.users'].browse(session.uid) + if openerp.tools.misc.consteq(self._compute_session_token(session.sid), session.session_token): + return True + self._invalidate_session_cache() + return False diff --git a/openerp/tests/common.py b/openerp/tests/common.py index 3cb271e075d..866fbd012ff 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -26,6 +26,7 @@ import werkzeug import openerp from openerp import api +from openerp.service import security from openerp.modules.registry import RegistryManager _logger = logging.getLogger(__name__) @@ -291,7 +292,7 @@ class HttpCase(TransactionCase): session.db = db session.uid = uid session.login = user - session.password = password + session.session_token = uid and security.compute_session_token(session, self.env) session.context = Users.context_get(self.cr, uid) or {} session.context['uid'] = uid session._fix_lang(session.context) From 4ca9ee2548cda201133ff20a42e404b005082b59 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 29 Jun 2018 09:30:42 +0200 Subject: [PATCH 5/5] [FIX] google_calendar: backport 052bc38805 to 9.0 Backport following opw-1851612 [FIX] google_calendar: do not create an event with an invalid id The id is useful to update existing events but sometimes we are getting some ids that are not accepted by Google Getting an error: odoo.addons.google_account.models.google_service: Bad google request : { "error": { "errors": [ { "domain": "global", "reason": "invalid", "message": "Invalid resource id value." } ], "code": 400, "message": "Invalid resource id value." } } Looks like existing events can have a _ in their id but new one, no longer. It seems that these events are created by outlook calendar when synchronized with Google Calendar. --- addons/google_calendar/google_calendar.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index 39f4a075ba5..17eeab5cacc 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -256,7 +256,8 @@ class google_calendar(osv.AbstractModel): if not self.get_need_synchro_attendee(cr, uid, context=context): data.pop("attendees") if isCreating: - other_google_ids = [other_att.google_internal_event_id for other_att in event.attendee_ids if other_att.google_internal_event_id] + other_google_ids = [other_att.google_internal_event_id for other_att in event.attendee_ids + if other_att.google_internal_event_id and not other_att.google_internal_event_id.startswith('_')] if other_google_ids: data["id"] = other_google_ids[0] return data @@ -627,7 +628,8 @@ class google_calendar(osv.AbstractModel): ('event_id.final_date', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ], context=context_norecurrent) for att in att_obj.browse(cr, uid, my_att_ids, context=context): - other_google_ids = [other_att.google_internal_event_id for other_att in att.event_id.attendee_ids if other_att.google_internal_event_id and other_att.id != att.id] + other_google_ids = [other_att.google_internal_event_id for other_att in att.event_id.attendee_ids if + other_att.google_internal_event_id and other_att.id != att.id and not other_att.google_internal_event_id.startswith('_')] for other_google_id in other_google_ids: if self.get_one_event_synchro(cr, uid, other_google_id, context=context): att_obj.write(cr, uid, [att.id], {'google_internal_event_id': other_google_id})