Commit 240a8d26 authored by Vincent Pelletier's avatar Vincent Pelletier

{,Propertied}User: Reduce the overhead from Developer role processing.

getRoles is called a lot (on every restricted access, so hundreds of times
per transaction), it is definitely not the right place to do extra
computation, especially when their result does not change from one call
to the next (configuration should only change on process restart, so not
during a transaction - and even if it someday did, it should be fine to
wait for next transaction for it to take effect).
Instead, do the extra work when creating the user (typically once per
transaction).
Also, modernise python syntax (simplifications & style).
Also, reduce code duplication from ERP5Security.ERP5UserFactory.
parent e793b164
...@@ -19,7 +19,6 @@ from Products.ERP5Type.Globals import InitializeClass ...@@ -19,7 +19,6 @@ from Products.ERP5Type.Globals import InitializeClass
from Acquisition import aq_inner, aq_parent from Acquisition import aq_inner, aq_parent
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.PageTemplates.PageTemplateFile import PageTemplateFile from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from App.config import getConfiguration
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products.PluggableAuthService.utils import classImplements from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.interfaces.plugins import IUserFactoryPlugin from Products.PluggableAuthService.interfaces.plugins import IUserFactoryPlugin
...@@ -52,151 +51,6 @@ class ERP5User(PropertiedUser): ...@@ -52,151 +51,6 @@ class ERP5User(PropertiedUser):
_user_path = None _user_path = None
_login_path = None _login_path = None
def getRolesInContext( self, object ):
""" Return the list of roles assigned to the user.
For ERP5, we check if a _getAcquireLocalRoles is defined on the object.
"""
user_id = self.getId()
# [ x.getId() for x in self.getGroups() ]
group_ids = self.getGroups()
principal_ids = list( group_ids )
principal_ids.insert( 0, user_id )
local = {}
object = aq_inner( object )
while 1:
local_roles = getattr( object, '__ac_local_roles__', None )
if local_roles:
if callable( local_roles ):
local_roles = local_roles()
dict = local_roles or {}
for principal_id in principal_ids:
for role in dict.get( principal_id, [] ):
local[ role ] = 1
# patch by Klaus for LocalRole blocking
if getattr(object, '_getAcquireLocalRoles', None) is not None:
if not object._getAcquireLocalRoles():
break
inner = aq_inner( object )
parent = aq_parent( inner )
if parent is not None:
object = parent
continue
new = getattr( object, 'im_self', None )
if new is not None:
object = aq_inner( new )
continue
break
# Patched: Developer role should not never be available as local role
local.pop('Developer', None)
return list( self.getRoles() ) + local.keys()
def allowed( self, object, object_roles=None ):
""" Check whether the user has access to object.
As for getRolesInContext, we take into account _getAcquireLocalRoles for
ERP5.
"""
if self.getUserName() == ERP5Security.SUPER_USER:
# super user is allowed to accesss any object
return 1
if object_roles is _what_not_even_god_should_do:
return 0
# Short-circuit the common case of anonymous access.
if object_roles is None or 'Anonymous' in object_roles:
return 1
# Check for Developer Role, see patches.User for rationale
# XXX-arnau: copy/paste
object_roles = set(object_roles)
if 'Developer' in object_roles:
object_roles.remove('Developer')
product_config = getattr(getConfiguration(), 'product_config', None)
if product_config:
config = product_config.get('erp5')
if config and self.getId() in config.developer_list:
return 1
# Provide short-cut access if object is protected by 'Authenticated'
# role and user is not nobody
if 'Authenticated' in object_roles and (
self.getUserName() != 'Anonymous User'):
return 1
# Check for ancient role data up front, convert if found.
# This should almost never happen, and should probably be
# deprecated at some point.
if 'Shared' in object_roles:
object_roles = self._shared_roles(object)
if object_roles is None or 'Anonymous' in object_roles:
return 1
# Check for a role match with the normal roles given to
# the user, then with local roles only if necessary. We
# want to avoid as much overhead as possible.
user_roles = self.getRoles()
for role in object_roles:
if role in user_roles:
if self._check_context(object):
return 1
return None
# Still have not found a match, so check local roles. We do
# this manually rather than call getRolesInContext so that
# we can incur only the overhead required to find a match.
inner_obj = aq_inner( object )
user_id = self.getId()
# [ x.getId() for x in self.getGroups() ]
group_ids = self.getGroups()
principal_ids = list( group_ids )
principal_ids.insert( 0, user_id )
while 1:
local_roles = getattr( inner_obj, '__ac_local_roles__', None )
if local_roles:
if callable( local_roles ):
local_roles = local_roles()
dict = local_roles or {}
for principal_id in principal_ids:
local_roles = dict.get( principal_id, [] )
for role in object_roles:
if role in local_roles:
if self._check_context( object ):
return 1
return 0
# patch by Klaus for LocalRole blocking
if getattr(inner_obj, '_getAcquireLocalRoles', None) is not None:
if not inner_obj._getAcquireLocalRoles():
break
inner = aq_inner( inner_obj )
parent = aq_parent( inner )
if parent is not None:
inner_obj = parent
continue
new = getattr( inner_obj, 'im_self', None )
if new is not None:
inner_obj = aq_inner( new )
continue
break
return None
def getUserValue(self): def getUserValue(self):
""" -> user document """ -> user document
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
# Locale roles acquisition patch for PAS # Locale roles acquisition patch for PAS
from Acquisition import aq_inner, aq_parent from Acquisition import aq_inner, aq_parent
from App.config import getConfiguration
try: try:
from Products.PluggableAuthService.PropertiedUser import PropertiedUser from Products.PluggableAuthService.PropertiedUser import PropertiedUser
from Products.PluggableAuthService.PropertiedUser import\ from Products.PluggableAuthService.PropertiedUser import\
...@@ -25,7 +25,10 @@ try: ...@@ -25,7 +25,10 @@ try:
except ImportError: except ImportError:
PropertiedUser = None PropertiedUser = None
def getRolesInContext( self, object ): TRUE_LAMBDA = lambda: True
DEVELOPER_ROLE_ID = 'Developer'
def getRolesInContext(self, object):
""" Return the list of roles assigned to the user. """ Return the list of roles assigned to the user.
...@@ -37,83 +40,33 @@ def getRolesInContext( self, object ): ...@@ -37,83 +40,33 @@ def getRolesInContext( self, object ):
o Ripped off from AccessControl.User.BasicUser, which provides o Ripped off from AccessControl.User.BasicUser, which provides
no other extension mechanism. :( no other extension mechanism. :(
""" """
user_id = self.getId() principal_id_list = [self.getId()]
# [ x.getId() for x in self.getGroups() ] principal_id_list.extend(self.getGroups())
group_ids = self.getGroups() result = set()
object = aq_inner(object)
principal_ids = list( group_ids )
principal_ids.insert( 0, user_id )
local ={}
object = aq_inner( object )
while 1: while 1:
local_role_dict = getattr(object, '__ac_local_roles__', None)
local_roles = getattr( object, '__ac_local_roles__', None ) if local_role_dict:
if callable(local_role_dict):
if local_roles: local_role_dict = local_role_dict() or {}
for principal_id in principal_id_list:
if callable( local_roles ): result.update(local_role_dict.get(principal_id, ()))
local_roles = local_roles() if getattr(object, '_getAcquireLocalRoles', TRUE_LAMBDA)():
parent = aq_parent(aq_inner(object))
dict = local_roles or {} if parent is not None:
object = parent
for principal_id in principal_ids: continue
for role in dict.get( principal_id, [] ): new = getattr(object, '__self__', None)
local[ role ] = 1 if new is not None:
object = aq_inner(new)
# patch by Klaus for LocalRole blocking continue
_getAcquireLocalRoles = getattr(object, '_getAcquireLocalRoles', None)
if _getAcquireLocalRoles is not None:
if not _getAcquireLocalRoles():
break
inner = aq_inner( object )
parent = aq_parent( inner )
if parent is not None:
object = parent
continue
new = getattr( object, 'im_self', None )
if new is not None:
object = aq_inner( new )
continue
break break
# Patched: Developer role should never be available as local role
result.discard(DEVELOPER_ROLE_ID)
result.update(self.getRoles())
return list(result)
# Patched: Developer role should not never be available as local role def allowed(self, object, object_roles=None):
local.pop('Developer', None)
return list( self.getRoles() ) + local.keys()
from App.config import getConfiguration
def getRoles( self ):
""" -> [ role ]
o Include only "global" roles.
"""
role_tuple = self._roles.keys()
if role_tuple:
product_config = getattr(getConfiguration(), 'product_config', None)
if product_config:
config = product_config.get('erp5')
if config:
role_set = set(role_tuple)
user_id = self.getId()
if config and user_id in config.developer_list:
role_set.add('Developer')
elif user_id in role_set:
role_set.remove('Developer')
return role_set
return role_tuple
def allowed(self, object, object_roles=None ):
""" Check whether the user has access to object. """ Check whether the user has access to object.
o The user must have one of the roles in object_roles to allow access. o The user must have one of the roles in object_roles to allow access.
...@@ -131,16 +84,7 @@ def allowed(self, object, object_roles=None ): ...@@ -131,16 +84,7 @@ def allowed(self, object, object_roles=None ):
if object_roles is None or 'Anonymous' in object_roles: if object_roles is None or 'Anonymous' in object_roles:
return 1 return 1
# Check for Developer Role, see patches.User for rationale
# XXX-arnau: copy/paste
object_roles = set(object_roles) object_roles = set(object_roles)
if 'Developer' in object_roles:
object_roles.remove('Developer')
product_config = getattr(getConfiguration(), 'product_config', None)
if product_config:
config = product_config.get('erp5')
if config and self.getId() in config.developer_list:
return 1
# Provide short-cut access if object is protected by 'Authenticated' # Provide short-cut access if object is protected by 'Authenticated'
# role and user is not nobody # role and user is not nobody
...@@ -156,75 +100,59 @@ def allowed(self, object, object_roles=None ): ...@@ -156,75 +100,59 @@ def allowed(self, object, object_roles=None ):
if object_roles is None or 'Anonymous' in object_roles: if object_roles is None or 'Anonymous' in object_roles:
return 1 return 1
# Check for a role match with the normal roles given to # Check global roles.
# the user, then with local roles only if necessary. We if object_roles.intersection(self.getRoles()):
# want to avoid as much overhead as possible. return self._check_context(object)
user_roles = self.getRoles() # Do not match Developer as a local role.
for role in object_roles: object_roles.discard(DEVELOPER_ROLE_ID)
if role in user_roles:
if self._check_context(object): check_context = self._check_context
return 1 # Check local roles.
return None inner_obj = aq_inner(object)
principal_id_list = [self.getId()]
# Still have not found a match, so check local roles. We do principal_id_list.extend(self.getGroups())
# this manually rather than call getRolesInContext so that
# we can incur only the overhead required to find a match.
inner_obj = aq_inner( object )
user_id = self.getId()
# [ x.getId() for x in self.getGroups() ]
group_ids = self.getGroups()
principal_ids = list( group_ids )
principal_ids.insert( 0, user_id )
while 1: while 1:
local_role_dict = getattr(inner_obj, '__ac_local_roles__', None)
local_roles = getattr( inner_obj, '__ac_local_roles__', None ) if local_role_dict:
if callable(local_role_dict):
if local_roles: local_role_dict = local_role_dict() or {}
for principal_id in principal_id_list:
if callable( local_roles ): for role in object_roles.intersection(
local_roles = local_roles() local_role_dict.get(principal_id, ()),
):
dict = local_roles or {} return int(bool(check_context(object)))
if getattr(inner_obj, '_getAcquireLocalRoles', TRUE_LAMBDA)():
for principal_id in principal_ids: parent = aq_parent(aq_inner(inner_obj))
local_roles = dict.get( principal_id, [] )
for role in object_roles:
if role in local_roles:
if self._check_context( object ):
return 1
return 0
# patch by Klaus for LocalRole blocking
_getAcquireLocalRoles = getattr(object, '_getAcquireLocalRoles', None)
if _getAcquireLocalRoles is not None:
if not _getAcquireLocalRoles():
break
inner = aq_inner( inner_obj )
parent = aq_parent( inner )
if parent is not None: if parent is not None:
inner_obj = parent inner_obj = parent
continue continue
new = getattr(inner_obj, '__self__', None)
new = getattr( inner_obj, 'im_self', None )
if new is not None: if new is not None:
inner_obj = aq_inner( new ) inner_obj = aq_inner(new)
continue continue
break
break
return None return None
orig_PropertiedUser__init__ = getattr(PropertiedUser, '__init__', None)
def PropertiedUser__init__(self, id, login=None):
orig_PropertiedUser__init__(self, id, login)
if id in getattr(
getattr(
getConfiguration(),
'product_config',
{},
).get('erp5'),
'developer_list',
(),
):
self._roles[DEVELOPER_ROLE_ID] = 1
orig_PropertiedUser__addRoles = getattr(PropertiedUser, '_addRoles', None)
def PropertiedUser__addRoles(self, roles=()):
orig_PropertiedUser__addRoles(self, (x for x in roles if x != DEVELOPER_ROLE_ID))
if PropertiedUser is not None: if PropertiedUser is not None:
PropertiedUser.getRolesInContext = getRolesInContext PropertiedUser.getRolesInContext = getRolesInContext
PropertiedUser.allowed = allowed PropertiedUser.allowed = allowed
PropertiedUser.getRoles = getRoles PropertiedUser.__init__ = PropertiedUser__init__
PropertiedUser._addRoles = PropertiedUser__addRoles
...@@ -13,7 +13,14 @@ ...@@ -13,7 +13,14 @@
# #
############################################################################## ##############################################################################
from AccessControl.User import BasicUser from threading import local
from Acquisition import aq_inner, aq_parent
from AccessControl.PermissionRole import _what_not_even_god_should_do
from AccessControl.User import BasicUser, SimpleUser
from App.config import getConfiguration
from ..TransactionalVariable import TransactionalVariable
DEVELOPER_ROLE_ID = 'Developer'
BasicUser_allowed = BasicUser.allowed BasicUser_allowed = BasicUser.allowed
def allowed(self, object, object_roles=None): def allowed(self, object, object_roles=None):
...@@ -22,81 +29,82 @@ def allowed(self, object, object_roles=None): ...@@ -22,81 +29,82 @@ def allowed(self, object, object_roles=None):
and remove it, as it should never be acquired anyhow, before calling the and remove it, as it should never be acquired anyhow, before calling the
original method original method
""" """
# XXX-arnau: copy/paste (PropertiedUser) # Skip "self._check_context(object)"
if object_roles is not None: if (
object_roles = set(object_roles) object_roles is not _what_not_even_god_should_do and
if 'Developer' in object_roles: object_roles is not None and
object_roles.remove('Developer') DEVELOPER_ROLE_ID in set(object_roles or ()).intersection(self.getRoles())
product_config = getattr(getConfiguration(), 'product_config', None) ):
if product_config: return 1
config = product_config.get('erp5')
if config and self.getId() in config.developer_list:
return 1
return BasicUser_allowed(self, object, object_roles) return BasicUser_allowed(self, object, object_roles)
BasicUser.allowed = allowed BasicUser.allowed = allowed
from App.config import getConfiguration
from AccessControl.User import SimpleUser
SimpleUser_getRoles = SimpleUser.getRoles SimpleUser_getRoles = SimpleUser.getRoles
def getRoles(self): def getRoles(self, _transactional_variable_pool=local()):
""" """
Add Developer Role if the user has been explicitely set as Developer in Zope Add Developer Role if the user has been explicitely set as Developer in Zope
configuration file configuration file
""" """
role_tuple = SimpleUser_getRoles(self) role_tuple = tuple(
if role_tuple: x
product_config = getattr(getConfiguration(), 'product_config', None) for x in SimpleUser_getRoles(self)
if product_config: if x != DEVELOPER_ROLE_ID
config = product_config.get('erp5') )
if config: # Use our private transactional cache pool, to avoid code meddling with
role_set = set(role_tuple) # roles. Hide it in a default parameter value to make it harder to access
user_id = self.getId() # than just importing it from the module.
if config and user_id in config.developer_list: try:
role_set.add('Developer') tv = _transactional_variable_pool.instance
elif user_id in role_set: except AttributeError:
role_set.remove('Developer') tv = TransactionalVariable()
_transactional_variable_pool.instance = tv
return role_set try:
extra_role_tuple = tv['user_extra_role_tuple']
return role_tuple except KeyError:
tv['user_extra_role_tuple'] = extra_role_tuple = (
(DEVELOPER_ROLE_ID, )
if self.getId() in getattr(
getattr(
getConfiguration(),
'product_config',
{},
).get('erp5'),
'developer_list',
(),
) else
()
)
return role_tuple + extra_role_tuple
  • This code creates a cache entry whose content depends on the user but not the key. I have a problem in my ERP5 instance where I have Unauthorized while I'm zope.

    I created the following patch to have log:

    diff --git a/product/ERP5Type/patches/User.py b/product/ERP5Type/patches/User.py
    index 15733d6d7e..8dbe0c8542 100644
    --- a/product/ERP5Type/patches/User.py
    +++ b/product/ERP5Type/patches/User.py
    @@ -61,6 +61,7 @@ def getRoles(self, _transactional_variable_pool=local()):
         _transactional_variable_pool.instance = tv
       try:
         extra_role_tuple = tv['user_extra_role_tuple']
    +    print("ACCESS in cache (user %s, tv %s) : %s" % (self.getId(), id(tv), extra_role_tuple))
       except KeyError:
         tv['user_extra_role_tuple'] = extra_role_tuple = (
           (DEVELOPER_ROLE_ID, )
    @@ -75,6 +76,7 @@ def getRoles(self, _transactional_variable_pool=local()):
           ) else
           ()
         )
    +    print("ACCESS NOT in cache (user %s, tv %s)) : %s" % (self.getId(), id(tv), extra_role_tuple))
       return role_tuple + extra_role_tuple
     
     SimpleUser.getRoles = getRoles

    And here is the result of my log:

    ACCESS NOT in cache (user None, tv 140460701632912)) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user zope, tv 140460701632912) : ()
    ACCESS in cache (user zope, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ACCESS in cache (user None, tv 140460701632912) : ()
    ...

    See the 2 lines with zope in the middle...

  • /cc @jm who helped me debug

  • Oh, nice catch. Does the following improve the situation ? (you may want to apply by hand to keep your own changes)

    diff --git a/product/ERP5Type/patches/User.py b/product/ERP5Type/patches/User.py
    index 15733d6d7e..e12be532b0 100644
    --- a/product/ERP5Type/patches/User.py
    +++ b/product/ERP5Type/patches/User.py
    @@ -60,9 +60,9 @@ def getRoles(self, _transactional_variable_pool=local()):
         tv = TransactionalVariable()
         _transactional_variable_pool.instance = tv
       try:
    -    extra_role_tuple = tv['user_extra_role_tuple']
    +    extra_role_tuple = tv[('user_extra_role_tuple', self.getId())]
       except KeyError:
    -    tv['user_extra_role_tuple'] = extra_role_tuple = (
    +    tv[('user_extra_role_tuple', self.getId())] = extra_role_tuple = (
           (DEVELOPER_ROLE_ID, )
           if self.getId() in getattr(
             getattr(
  • Yes indeed, it works fine.

    ACCESS NOT in cache (user None, tv 139730790856808)) : ()
    ACCESS in cache (user None, tv 139730790856808) : ()
    ACCESS in cache (user None, tv 139730790856808) : ()
    ACCESS in cache (user None, tv 139730790856808) : ()
    ACCESS in cache (user None, tv 139730790856808) : ()
    ACCESS in cache (user None, tv 139730790856808) : ()
    ACCESS NOT in cache (user zope, tv 139730790856808)) : ('Developer',)
  • I don't understand the purpose of a cache in this function. Why not just have developer_list = ... at module level and do

    if self.getId() in developer_list:
      role_tuple += (DEVELOPER_ROLE_ID, )

    in the function.

    Edited by Julien Muchembled
  • IIRC this is a matter of initialisation order: at import time, Zope configuration would not be reachable yet.

    This said, a developer_list somewhere in this module could be filled upon first access, and used as a longer-lived cache. I guess I was being conservative compared to an initial getRoles which was accessing the configuration on every single call, in case this caused issues (...but not the kind of issue which happened here).

  • So still at module_level:

    class developer_list:
      def __contains__(self, item):
        global developer_list
        developer_list = ...
        return item in developer_list
    developer_list = developer_list()
  • Something like this, yes. Feel free to finalise and commit.

Please register or sign in to reply
SimpleUser.getRoles = getRoles SimpleUser.getRoles = getRoles
SimpleUser_getRolesInContext = SimpleUser.getRolesInContext
def getRolesInContext(self, object): def getRolesInContext(self, object):
""" """
Return the list of roles assigned to the user, including local roles Return the list of roles assigned to the user, including local roles
assigned in context of the passed in object. assigned in context of the passed in object.
""" """
userid=self.getId() userid = self.getId()
roles=self.getRoles() result = set()
local={} object = aq_inner(object)
object=getattr(object, 'aq_inner', object)
while 1: while 1:
local_roles = getattr(object, '__ac_local_roles__', None) local_role_dict = getattr(object, '__ac_local_roles__', None)
if local_roles: if local_role_dict:
if callable(local_roles): if callable(local_role_dict):
local_roles=local_roles() local_role_dict = local_role_dict() or {}
dict=local_roles or {} result.update(local_role_dict.get(userid, ()))
for r in dict.get(userid, []): parent = aq_parent(aq_inner(object))
local[r]=1
inner = getattr(object, 'aq_inner', object)
parent = getattr(inner, '__parent__', None)
if parent is not None: if parent is not None:
object = parent object = parent
continue continue
if hasattr(object, 'im_self'): new = getattr(object, '__self__', None)
object=object.im_self if new is not None:
object=getattr(object, 'aq_inner', object) object = aq_inner(new)
continue continue
break break
# Patched: Developer role should never be available as local role # Patched: Developer role should never be available as local role
local.pop('Developer', None) result.discard(DEVELOPER_ROLE_ID)
roles=list(roles) + local.keys() result.update(self.getRoles())
  • If I read correctly, this changed the logic. Before this change, Developer role was removed from local only. If I understand the comment (# Patched: Developer role should never be available as local role) it means that if the user has Developer as a global role (because the user is listed in zope.conf) the role is returned, but not in case the Developer role was set as a local role.

    I think all this was a protection against a user adding themselves a Developer local role on a document to get the permission to edit it, but what matters for security is the result allowed check and allowed does not use getRolesInContext.

    This made getRolesInContext return less roles and has been like this for 3 years and we never noticed any problem, to me this means this SimpleUser.getRolesInContext patch was not necessary.

  • If I read correctly,

    I did not read correctly, forget about this comment

Please register or sign in to reply
return roles return list(result)
SimpleUser.getRolesInContext = getRolesInContext SimpleUser.getRolesInContext = getRolesInContext
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment