How to restrict multiple logins of a user in OpenERP?
Hi all recently I was assigned the task of restricting multiple logins of a single user in OpenERP. Since I'm a newbie to OpenERP, I searched the web for some kick-start articles but for vain. That is why I'm summarizing my points in this, my first ever, blog.
My task in hand was pretty straight forward – restrict a user from logging in from multiple locations simultaneously and for the time-being needs only to be implemented for the web-client. For this task I'm using OpenERP v6.0.2. So my first task was to find out how the web-client worked in OpenERP. While investigating I found that the web-client was powered by “CherryPy”.
What is CherryPy?
This is the definition of CherryPy as in wikipedia.
"CherryPy is an object-oriented web application framework using the Python programming language. It is defined for rapid development of web applications by wrapping the HTTP protocol but stays at a low level and does not offer much more than what is defined in RFC 2616.”
A simple tutorial to get started with CherryPy can be found here. Going through this tutorial is highly recommended. (Some of you might have already got the idea about why I put the blog's title as “An Added Layer of Indirection” and for the rest go here.)
How to retrieve data from cookies?
To get a cookie all we have to do is give the following:
cookie = cherrypy.response.cookie
By examining the cookie we can find that it contains session id and expiration date. We can retrieve all these details as follows:
session_id = cookie['session_id'].value
expiration_date = cookie['session_id']['expires']
Here keep a note that the expiration_date retrieved is a string and not a datetime object.
How to retrieve data from RPC session?
Another piece of information that we need is the user id of the logged in user. This data can be retrieved as follows:
uid = rpc.session.storage['uid']
A Simple Design
Keeping all these informations in mind a simple design was developed.
-
Store the session id and expiration date in the users table in the database.
-
While logging in check whether the user has already logged (If a user has already logged in then the session id will not be null).
-
If the user is already logged in check whether that session has expired.
-
If the session has expired allow the user to log in else consider it as multiple log in and cancel the log in.
-
When the user is active update the expiration date in the database.
-
When the user logs out clear the session id and expiration date fields in the database.
What Now?
Its now time to get our hands a little dirty, bear with me. First we need to add two fields to the user model. So create a module(user_restriction) in the server (openerp-server/bin/addons) in which create a model inheriting res.users as follows:
class users(osv.osv):
_inherit = 'res.users'
_name = 'res.users'
_columns = {
'session_id' : fields.char('Session Id', size=100),
'expiration_date' : fields.datetime('Expiration Date'),
}
Some useful functions
Now we will be creating some useful functions that are to be used to do our task. For that create a file named user_restriction.py in the openerp-web (openerp-web/addons/openerp/controllers).
from openerp.utils import rpc
import cherrypy
import datetime
import pytz
DATE_FORMAT_GMT = '%a, %d %b %Y %H:%M:%S GMT'
DATE_FORMAT_ISO = '%Y-%m-%d %H:%M:%S'
MODULE_NAME = 'user_restriction'
def _get_user_id():
return rpc.session.storage['uid']
def _get_user_details():
return rpc.RPCProxy('res.users').read(_get_user_id())
def update_user_session(data):
rpc.RPCProxy('res.users').write(_get_user_id(), data, rpc.session.context)
def is_user_restriction_module_installed():
if not rpc.session.storage.get(MODULE_NAME):
user_restriction_module_ids = rpc.RPCProxy('ir.module.module').search([('name', '=', MODULE_NAME), ('state', '=', 'installed')])
user_details = _get_user_details()
rpc.session.storage[MODULE_NAME] = ((len(user_restriction_module_ids) == 1) and ('session_id' in user_details.keys()))
return rpc.session.storage[MODULE_NAME]
def _get_user():
return rpc.session.storage['user']
def _get_date_string(date_obj, date_format):
date_string = None
if date_obj:
date_string = date_obj.strftime(date_format)
return date_string
def _get_date_obj(date_string, date_format):
date_obj = None
if date_string:
date_obj = datetime.datetime.strptime(date_string, date_format)
return date_obj
def get_date_obj(date_string):
return _get_date_obj(date_string, DATE_FORMAT_GMT)
def _get_local_time_in_gmt():
current_gmt_date = datetime.datetime.now(pytz.timezone('GMT'))
current_gmt_date_string = _get_date_string(current_gmt_date, DATE_FORMAT_GMT)
current_date = _get_date_obj(current_gmt_date_string, DATE_FORMAT_GMT)
return current_date
def has_user_loged_in(action):
restrict_login = False
if action == 'login' and is_user_restriction_module_installed():
result = _get_user_details()
previous_expiration_date_string = result['expiration_date']
previous_expiration_date = _get_date_obj(previous_expiration_date_string, DATE_FORMAT_ISO)
cookie = cherrypy.response.cookie
current_session_id = cookie['session_id'].value
current_expiration_date = _get_date_obj(cookie['session_id']['expires'], DATE_FORMAT_GMT)
current_date = _get_local_time_in_gmt()
if previous_expiration_date and (previous_expiration_date - current_date > datetime.timedelta(0)):
# Multiple Login.
restrict_login = True
else:
# Write Current Expiration Date to database. Login success.
data = {'session_id' : current_session_id, 'expiration_date' : str(current_expiration_date)}
update_user_session(data)
return restrict_login
def clear_session():
if is_user_restriction_module_installed():
result = _get_user_details()
cookie = cherrypy.response.cookie
current_session_id = cookie['session_id'].value
if result['session_id'] and result['session_id'] == current_session_id:
data = {'session_id' : None, 'expiration_date' : None}
update_user_session(data)
All the functions are simple and straight forward and the names itself gives the idea about what the function does. The two important functions to consider here are the has_user_logged_in and clear_session. The has_user_logged_in function does the steps ii and iii and the clear_session function performs the step vi mentioned in the design above.
Now we need to find the right places in the web-client from where we can call our functions described above. I followed a trial and error method to find those places and got the following which I thought served for my purpose.
First task was to find what is happening when a user enters a wrong user name and/or password during login. Investigating this scenario I found the following function in openerp-web/addons/openerp/controllers/utils.py in line no. 83.
def secured(fn):
"""A Decorator to make a SecuredController controller method secured.
"""
def clear_login_fields(kw):
if not kw.get('login_action'):
return
for k in ('db', 'user', 'password'):
kw.pop(k, None)
for k in kw.keys():
if k.startswith('login_'):
del kw[k]
def get_orig_args(kw):
if not kw.get('login_action'):
return kw
new_kw = kw.copy()
clear_login_fields(new_kw)
return new_kw
def wrapper(*args, **kw):
"""The wrapper function to secure exposed methods
"""
if rpc.session.is_logged() and kw.get('login_action') != 'login':
# User is logged in; allow access
clear_login_fields(kw)
return fn(*args, **kw)
else:
action = kw.get('login_action', '')
# get some settings from cookies
try:
db = cherrypy.request.cookie['terp_db'].value
user = cherrypy.request.cookie['terp_user'].value
except:
db = ''
user = ''
db = kw.get('db', db)
user = ustr(kw.get('user', user))
password = kw.get('password', '')
# See if the user just tried to log in
if rpc.session.login(db, user, password) <= 0:
# Bad login attempt
if action == 'login':
message = _("Bad username or password")
return login(cherrypy.request.path_info, message=message,
db=db, user=user, action=action, origArgs=get_orig_args(kw))
else:
message = ''
kwargs = {}
if action: kwargs['action'] = action
if message: kwargs['message'] = message
base = cherrypy.request.path_info
if cherrypy.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
cherrypy.response.status = 401
next_key = 'next'
else:
cherrypy.response.status = 303
next_key = 'location' # login?location is the redirection destination w/o next
if base and base != '/' and cherrypy.request.method == 'GET':
kwargs[next_key] = "%s?%s" % (base, cherrypy.request.query_string)
login_url = openobject.tools.url(
'/openerp/login', db=db, user=user, **kwargs
)
cherrypy.response.headers['Location'] = login_url
return """
<html>
<head>
<script type="text/javascript">
window.location.href="%s"
</script>
</head>
<body>
</body>
</html>
"""%(login_url)
# Authorized. Set db, user name in cookies
cookie = cherrypy.response.cookie
cookie['terp_db'] = db
cookie['terp_user'] = user.encode('utf-8')
cookie['terp_db']['max-age'] = 3600
cookie['terp_user']['max-age'] = 3600
cookie['terp_db']['path'] = '/'
cookie['terp_user']['path'] = '/'
# User is now logged in, so show the content
clear_login_fields(kw)
return fn(*args, **kw)
return tools.decorated(wrapper, fn, secured=True)
If we look at it we can see that when the user is authorized then some data is set in the cookie. So I thought I will just validate the multiple login just above that (after line no. 165). So we can add the following code there.
import user_restriction
restrict_login = user_restriction.has_user_logged_in(action)
if restrict_login :
rpc.session.logout()
message = _("%s already logged in.\nEither from another place or from a different browser. Log out from that session and try again."%(user))
return login(cherrypy.request.path_info, message=message, db=db, user=user, action=action, origArgs=get_orig_args(kw))
Next task is to clear the session details when the user logs out. By looking at the source we can find that the logout function is defined in the class Root(openerp-web/addons/openerp/controllers/root.py line no. 162).
The original code for the function is as follows.
def logout(self):
""" Logout method, will terminate the current session.
"""
rpc.session.logout()
raise redirect('/')
We can modify it to call our clear_session which we defined in user_restriction.py file. The modified code will be like this.
def logout(self):
""" Logout method, will terminate the current session.
"""
import user_restriction
if rpc.session.storage.get(user_restriction.MODULE_NAME):
rpc.session.storage.delete(user_restriction.MODULE_NAME)
user_restriction.clear_session()
rpc.session.logout()
raise redirect('/')
So now our major problems are solved. The next task is to update the expiration_date of the user session when the user is active, that is, whenever a user performs an operation. There can be a variety of operations a user can perform so finding a place to update for each operation is not a solution. So I kept looking for a central point or a base class through which all the calls will be passing through. Fortunately such a class do exist in the web-client – BaseController class (openerp-web/openobject/controllers/_base.py line no. 42). The BaseController class has only one method _get_path(). The code of the original method is as follows.
def _get_path(self):
return self._cp_path
We modify it as follows.
def _get_path(self):
from openerp.controllers import user_restriction
import cherrypy
if user_restriction.is_user_restriction_module_installed():
cookie = cherrypy.response.cookie
data = {'session_id' : cookie['session_id'].value, 'expiration_date' : str(user_restriction.get_date_obj(cookie['session_id']['expires']))}
user_restriction.update_user_session(data)
return self._cp_path
With all these changes in place we can successfully stop the multiple login of a user to OpenERP.
Some Helpful Links
http://www.cherrypy.org/chrome/common/2.1/docs/book/chunk/index.html
http://www.cherrypy.org/wiki/CherryPySessions
http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
Hi! Great tutorial, I wonder
Hi! Great tutorial, I wonder if it is possible to do this in openerp 7. Thank you!
Thank you very much, it's
Thank you very much, it's very good for me
wonderful submit, very
wonderful submit, very informative. I ponder why the opposite specialists of this sector don't notice this. You should continue your writing. I'm confident, you have a huge readers' base already!
well written Vinod. A good
well written Vinod. A good feature indeed..
Thanks Jean.
Thanks Jean.
Hi vinod i read ur blog u
Hi vinod
i read ur blog u have explained beautyfully........
i have followed steps exactly what u have explained
but i am not able achive the expected result
can u help me out plz