India

ZestyBeanz Technologies Pvt Ltd 4th Floor, Nila, Technopark Thiruvananthapuram,
India – 695581
Phone: +91 471 4063254
Fax : +91 471 2700171

   .

ZestyBeanz Technologies Pvt Ltd
61/3236, Manikkath Cross Road
Ravipuram, Kochi, India - 682016
Phone: +91 484 4063254

  UAE

Zesty Labs
Office # 2003, Millennium Plaza Building
Sheikh Zayed Rd, Dubai, UAE
Phone: +971 4333 2222​
Mobile: +971-52-7553466

  Germany​

ZestyBeanz Technologies GmbH
Reuterstraße 1
90408 Nürnberg
Fon: +49 911 4801 444
Fax: +49 911 4801 445

How to restrict multiple logins of a user in OpenERP?

Contact Form


Vinod_Kumar's picture

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:

  1. 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:

  1. session_id = cookie['session_id'].value
  2. 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:

  1. uid = rpc.session.storage['uid']

A Simple Design

Keeping all these informations in mind a simple design was developed.

  1. Store the session id and expiration date in the users table in the database.

  2. 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).

  3. If the user is already logged in check whether that session has expired.

  4. If the session has expired allow the user to log in else consider it as multiple log in and cancel the log in.

  5. When the user is active update the expiration date in the database.

  6. 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:

  1. class users(osv.osv): 
  2.     _inherit = 'res.users' 
  3.     _name = 'res.users' 
  4.  
  5.     _columns = { 
  6.                 'session_id' : fields.char('Session Id', size=100), 
  7.                 'expiration_date' : fields.datetime('Expiration Date'), 
  8.                 } 

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).

  1. from openerp.utils import rpc
  2. import cherrypy
  3. import datetime
  4. import pytz
  5. DATE_FORMAT_GMT = '%a, %d %b %Y %H:%M:%S GMT'
  6. DATE_FORMAT_ISO = '%Y-%m-%d %H:%M:%S'
  7. MODULE_NAME = 'user_restriction'
  8.  
  9. def _get_user_id():
  10.     return rpc.session.storage['uid']
  11.  
  12. def _get_user_details():
  13.     return rpc.RPCProxy('res.users').read(_get_user_id())
  14.  
  15. def update_user_session(data):
  16.     rpc.RPCProxy('res.users').write(_get_user_id(), data, rpc.session.context)
  17.  
  18. def is_user_restriction_module_installed():
  19.     if not rpc.session.storage.get(MODULE_NAME):
  20.         user_restriction_module_ids = rpc.RPCProxy('ir.module.module').search([('name', '=', MODULE_NAME), ('state', '=', 'installed')])
  21.         user_details = _get_user_details()
  22.         rpc.session.storage[MODULE_NAME] = ((len(user_restriction_module_ids) == 1) and ('session_id' in user_details.keys()))
  23.  
  24.     return rpc.session.storage[MODULE_NAME]
  25.  
  26. def _get_user(): 
  27.     return rpc.session.storage['user'] 
  28.  
  29. def _get_date_string(date_obj, date_format): 
  30.     date_string = None 
  31.     if date_obj: 
  32.         date_string = date_obj.strftime(date_format)
  33.     return date_string
  34.  
  35. def _get_date_obj(date_string, date_format):
  36.     date_obj = None
  37.     if date_string:
  38.         date_obj = datetime.datetime.strptime(date_string, date_format)
  39.     return date_obj
  40.  
  41. def get_date_obj(date_string):
  42.     return _get_date_obj(date_string, DATE_FORMAT_GMT)
  43.  
  44. def _get_local_time_in_gmt():
  45.     current_gmt_date = datetime.datetime.now(pytz.timezone('GMT'))
  46.     current_gmt_date_string = _get_date_string(current_gmt_date, DATE_FORMAT_GMT)
  47.     current_date = _get_date_obj(current_gmt_date_string, DATE_FORMAT_GMT)
  48.     return current_date
  49.  
  50. def has_user_loged_in(action):
  51.     restrict_login = False
  52.     if action == 'login' and is_user_restriction_module_installed():
  53.         result = _get_user_details()
  54.         previous_expiration_date_string = result['expiration_date']
  55.         previous_expiration_date = _get_date_obj(previous_expiration_date_string, DATE_FORMAT_ISO) 
  56.  
  57.         cookie = cherrypy.response.cookie
  58.         current_session_id = cookie['session_id'].value
  59.         current_expiration_date = _get_date_obj(cookie['session_id']['expires'], DATE_FORMAT_GMT) 
  60.         current_date = _get_local_time_in_gmt()
  61.  
  62.         if previous_expiration_date and (previous_expiration_date - current_date > datetime.timedelta(0)):
  63.             # Multiple Login.
  64.             restrict_login = True
  65.         else:
  66.             # Write Current Expiration Date to database. Login success. 
  67.             data = {'session_id' : current_session_id, 'expiration_date' : str(current_expiration_date)} 
  68.             update_user_session(data)
  69.     return restrict_login
  70.  
  71. def clear_session():
  72.     if is_user_restriction_module_installed():
  73.         result = _get_user_details()
  74.         cookie = cherrypy.response.cookie
  75.         current_session_id = cookie['session_id'].value
  76.         if result['session_id'] and result['session_id'] == current_session_id:
  77.             data = {'session_id' : None, 'expiration_date' : None}
  78.             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.

  1. def secured(fn):
  2.  
  3.     """A Decorator to make a SecuredController controller method secured.
  4.  
  5.     """
  6.  
  7.     def clear_login_fields(kw):
  8.  
  9.  
  10.  
  11.         if not kw.get('login_action'):
  12.  
  13.             return
  14.  
  15.  
  16.  
  17.         for k in ('db', 'user', 'password'):
  18.  
  19.             kw.pop(k, None)
  20.  
  21.         for k in kw.keys():
  22.  
  23.             if k.startswith('login_'):
  24.  
  25.                 del kw[k]
  26.  
  27.  
  28.  
  29.     def get_orig_args(kw):
  30.  
  31.         if not kw.get('login_action'):
  32.  
  33.             return kw
  34.  
  35.  
  36.  
  37.         new_kw = kw.copy()
  38.  
  39.         clear_login_fields(new_kw)
  40.  
  41.         return new_kw
  42.  
  43.  
  44.  
  45.     def wrapper(*args, **kw):
  46.  
  47.         """The wrapper function to secure exposed methods
  48.  
  49.         """
  50.  
  51.  
  52.  
  53.         if rpc.session.is_logged() and kw.get('login_action') != 'login':
  54.  
  55.             # User is logged in; allow access
  56.  
  57.             clear_login_fields(kw)
  58.  
  59.             return fn(*args, **kw)
  60.  
  61.         else:
  62.  
  63.             action = kw.get('login_action', '')
  64.  
  65.             # get some settings from cookies
  66.  
  67.             try:
  68.  
  69.                 db = cherrypy.request.cookie['terp_db'].value
  70.  
  71.                 user = cherrypy.request.cookie['terp_user'].value
  72.  
  73.             except:
  74.  
  75.                 db = ''
  76.  
  77.                 user = ''
  78.  
  79.  
  80.  
  81.             db = kw.get('db', db)
  82.  
  83.             user = ustr(kw.get('user', user))
  84.  
  85.             password = kw.get('password', '')
  86.  
  87.  
  88.  
  89.             # See if the user just tried to log in
  90.  
  91.             if rpc.session.login(db, user, password) <= 0:
  92.  
  93.                 # Bad login attempt
  94.  
  95.                 if action == 'login':
  96.  
  97.                     message = _("Bad username or password")
  98.  
  99.                     return login(cherrypy.request.path_info, message=message,
  100.  
  101.                         db=db, user=user, action=action, origArgs=get_orig_args(kw))
  102.  
  103.                 else:
  104.  
  105.                     message = ''
  106.  
  107.  
  108.  
  109.                 kwargs = {}
  110.  
  111.                 if action: kwargs['action'] = action
  112.  
  113.                 if message: kwargs['message'] = message
  114.  
  115.                 base = cherrypy.request.path_info
  116.  
  117.                 if cherrypy.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
  118.  
  119.                     cherrypy.response.status = 401
  120.  
  121.                     next_key = 'next'
  122.  
  123.                 else:
  124.  
  125.                     cherrypy.response.status = 303
  126.  
  127.                     next_key = 'location' # login?location is the redirection destination w/o next
  128.  
  129.  
  130.  
  131.                 if base and base != '/' and cherrypy.request.method == 'GET':
  132.  
  133.                     kwargs[next_key] = "%s?%s" % (base, cherrypy.request.query_string)
  134.  
  135.  
  136.  
  137.                 login_url = openobject.tools.url(
  138.  
  139.                     '/openerp/login', db=db, user=user, **kwargs
  140.  
  141.                 )
  142.  
  143.                 cherrypy.response.headers['Location'] = login_url
  144.  
  145.                 return """
  146.  
  147.                     <html>
  148.  
  149.                         <head>
  150.  
  151.                             <script type="text/javascript">
  152.  
  153.                                 window.location.href="%s"
  154.  
  155.                             </script>
  156.  
  157.                         </head>
  158.  
  159.                         <body>
  160.  
  161.                         </body>
  162.  
  163.                     </html>
  164.  
  165.                 """%(login_url)
  166.  
  167.  
  168.  
  169.             # Authorized. Set db, user name in cookies
  170.  
  171.             cookie = cherrypy.response.cookie
  172.  
  173.             cookie['terp_db'] = db
  174.  
  175.             cookie['terp_user'] = user.encode('utf-8')
  176.  
  177.             cookie['terp_db']['max-age'] = 3600
  178.  
  179.             cookie['terp_user']['max-age'] = 3600
  180.  
  181.             cookie['terp_db']['path'] = '/'
  182.  
  183.             cookie['terp_user']['path'] = '/'
  184.  
  185.  
  186.  
  187.             # User is now logged in, so show the content
  188.  
  189.             clear_login_fields(kw)
  190.  
  191.             return fn(*args, **kw)
  192.  
  193.  
  194.  
  195.     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.

  1. import user_restriction
  2.  
  3. restrict_login = user_restriction.has_user_logged_in(action)
  4.  
  5. if restrict_login :
  6.  
  7.     rpc.session.logout()
  8.  
  9.     message = _("%s already logged in.\nEither from another place or from a different browser. Log out from that session and try again."%(user))
  10.  
  11.     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.

  1. def logout(self):
  2.  
  3.         """ Logout method, will terminate the current session.
  4.  
  5.         """
  6.  
  7.         rpc.session.logout()
  8.  
  9.         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.

  1. def logout(self):
  2.  
  3.         """ Logout method, will terminate the current session.
  4.  
  5.         """
  6.  
  7.         import user_restriction
  8.  
  9.         if rpc.session.storage.get(user_restriction.MODULE_NAME):
  10.  
  11.             rpc.session.storage.delete(user_restriction.MODULE_NAME)
  12.  
  13.         user_restriction.clear_session()
  14.  
  15.  
  16.         rpc.session.logout()
  17.  
  18.         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.

  1.     def _get_path(self):
  2.  
  3.         return self._cp_path

We modify it as follows.

  1.     def _get_path(self):
  2.  
  3.         from openerp.controllers import user_restriction
  4.  
  5.         import cherrypy
  6.  
  7.         if user_restriction.is_user_restriction_module_installed():
  8.  
  9.             cookie = cherrypy.response.cookie
  10.  
  11.             data = {'session_id' : cookie['session_id'].value, 'expiration_date' : str(user_restriction.get_date_obj(cookie['session_id']['expires']))}
  12.  
  13.             user_restriction.update_user_session(data)
  14.  
  15.  
  16.         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

http://tools.cherrypy.org/wiki/CASAuthentication

Tomas Del Bianco's picture

Hi! Great tutorial, I wonder

Hi! Great tutorial, I wonder if it is possible to do this in openerp 7. Thank you!

Anonymous's picture

Thank you very much, it's

Thank you very much, it's very good for me

Mei Mcgonigle's picture

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!

Jean Johnson's picture

well written Vinod. A good

well written Vinod. A good feature indeed..

Vinod_Kumar's picture

Thanks Jean.

Thanks Jean.

suraj's picture

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