diff --git a/addons/auth_crypt/models/res_users.py b/addons/auth_crypt/models/res_users.py index 708998acbde..711cfc9d88d 100644 --- a/addons/auth_crypt/models/res_users.py +++ b/addons/auth_crypt/models/res_users.py @@ -96,3 +96,6 @@ class ResUsers(models.Model): internally """ return default_crypt_context + + def _get_session_token_fields(self): + return super(ResUsers, self)._get_session_token_fields() | {'password_crypt'} diff --git a/addons/auth_oauth/models/res_users.py b/addons/auth_oauth/models/res_users.py index b10f19a28be..be756149ba3 100644 --- a/addons/auth_oauth/models/res_users.py +++ b/addons/auth_oauth/models/res_users.py @@ -125,3 +125,6 @@ class ResUsers(models.Model): res = self.sudo().search([('id', '=', self.env.uid), ('oauth_access_token', '=', password)]) if not res: raise + + def _get_session_token_fields(self): + return super(ResUsers, self)._get_session_token_fields() | {'oauth_access_token'} diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index 8a79813ba96..c621ca477f5 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -3,11 +3,13 @@ import pytz import datetime import logging +import hmac from collections import defaultdict from itertools import chain, repeat from lxml import etree from lxml.builder import E +from hashlib import sha256 from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationError @@ -375,6 +377,8 @@ class Users(models.Model): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) + if any(key in values for key in self._get_session_token_fields()): + self._invalidate_session_cache() return res @@ -385,6 +389,7 @@ class Users(models.Model): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) + self._invalidate_session_cache() return super(Users, self).unlink() @api.model @@ -510,6 +515,34 @@ class Users(models.Model): finally: cr.close() + 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.multi + def _invalidate_session_cache(self): + """ Clear the sessions cache """ + self._compute_session_token.clear_cache(self) + @api.model def change_password(self, old_passwd, new_passwd): """Change current user password. Old password must be provided explicitly diff --git a/odoo/http.py b/odoo/http.py index 780521db857..a9e9e8cf9ed 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -1046,7 +1046,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.uid = uid request.disable_db = False @@ -1061,7 +1061,16 @@ 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) + + # == 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) + self.pop('password') + # ================================================================================================= + # here we check if the session is still valid + if not security.check_session(self): + raise SessionExpiredException("Session expired") def logout(self, keep_db=False): for k in self.keys(): @@ -1074,7 +1083,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): diff --git a/odoo/service/security.py b/odoo/service/security.py index 264e88d42bd..229e4b84077 100644 --- a/odoo/service/security.py +++ b/odoo/service/security.py @@ -11,3 +11,16 @@ def login(db, login, password): def check(db, uid, passwd): res_users = odoo.registry(db)['res.users'] return res_users.check(db, uid, passwd) + +def compute_session_token(session): + with odoo.registry(session.db).cursor() as cr: + self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid) + return self._compute_session_token(session.sid) + +def check_session(session): + with odoo.registry(session.db).cursor() as cr: + self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid) + if odoo.tools.misc.consteq(self._compute_session_token(session.sid), session.session_token): + return True + self._invalidate_session_cache() + return False diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 5f961e39e41..37000441306 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -26,6 +26,8 @@ import werkzeug import odoo from odoo import api +from odoo.service import security + _logger = logging.getLogger(__name__) @@ -286,7 +288,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) session.context = env['res.users'].context_get() or {} session.context['uid'] = uid session._fix_lang(session.context)