############################################################################## # # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## """SecurityInfo objects and access control constants. SecurityInfo objects are used in class definitions to allow a declarative style of associating access control information with class attributes. More information on using SecurityInfo and guide to Zope security for developers can be found in the dev.zope.org "Declarative Security" project: http://dev.zope.org/Wikis/DevSite/Projects/DeclarativeSecurity While SecurityInfo objects largely remove the need for Python programmers to care about the underlying implementation, there are several constants defined that should be used by code that must set __roles__ attributes directly. (the constants are also accessible from the AccessControl namespace). The defined access control constants and their meanings are: ACCESS_PUBLIC: accessible from restricted code and possibly through the web (if object has a docstring) ACCESS_PRIVATE: accessible only from python code ACCESS_NONE: no access """ __version__='$Revision$'[11:-2] import Acquisition, PermissionRole, sys from zLOG import LOG, WARNING # Security constants - these are imported into the AccessControl # namespace and can be referenced as AccessControl.PUBLIC etc. ACCESS_NONE = PermissionRole._what_not_even_god_should_do ACCESS_PRIVATE = () ACCESS_PUBLIC = None _marker = [] class SecurityInfo(Acquisition.Implicit): """Encapsulate security information.""" __security_info__ = 1 __roles__ = ACCESS_PRIVATE def __init__(self): self.names = {} self.roles = {} def _setaccess(self, names, access): # Empty names list sets access to the class itself, named '' if not len(names): names = ('',) for name in names: if self.names.get(name, access) != access: LOG('SecurityInfo', WARNING, 'Conflicting security ' 'declarations for "%s"' % name) self._warnings = 1 self.names[name] = access declarePublic__roles__=ACCESS_PRIVATE def declarePublic(self, *names): """Declare names to be publicly accessible.""" self._setaccess(names, ACCESS_PUBLIC) declarePrivate__roles__=ACCESS_PRIVATE def declarePrivate(self, *names): """Declare names to be inaccessible to restricted code.""" self._setaccess(names, ACCESS_PRIVATE) declareProtected__roles__=ACCESS_PRIVATE def declareProtected(self, permission_name, name, *names): """Declare names to be associated with a permission.""" self._setaccess((name,) + names, permission_name) declareObjectPublic__roles__=ACCESS_PRIVATE def declareObjectPublic(self): """Declare the object to be publicly accessible.""" self._setaccess((), ACCESS_PUBLIC) declareObjectPrivate__roles__=ACCESS_PRIVATE def declareObjectPrivate(self): """Declare the object to be inaccessible to restricted code.""" self._setaccess((), ACCESS_PRIVATE) declareObjectProtected__roles__=ACCESS_PRIVATE def declareObjectProtected(self, permission_name): """Declare the object to be associated with a permission.""" self._setaccess((), permission_name) setPermissionDefault__roles__=ACCESS_PRIVATE def setPermissionDefault(self, permission_name, roles): """Declare default roles for a permission""" rdict = {} for role in roles: rdict[role] = 1 if self.roles.get(permission_name, rdict) != rdict: LOG('SecurityInfo', WARNING, 'Conflicting default role' 'declarations for permission "%s"' % permission_name) self._warnings = 1 self.roles[permission_name] = rdict setDefaultAccess__roles__=ACCESS_PRIVATE def setDefaultAccess(self, access): """Declare default attribute access policy. This should be a boolean value, a map of attribute names to booleans, or a callable (name, value) -> boolean. """ if type(access) == type(''): access = access.lower() if access == 'allow': access = 1 elif access == 'deny': access = 0 else: raise ValueError, "'allow' or 'deny' expected" self.access = access class ClassSecurityInfo(SecurityInfo): """Encapsulate security information for class objects.""" __roles__ = ACCESS_PRIVATE apply__roles__ = ACCESS_PRIVATE def apply(self, classobj): """Apply security information to the given class object.""" dict = classobj.__dict__ # Check the class for an existing __ac_permissions__ and # incorporate that if present to support older classes or # classes that haven't fully switched to using SecurityInfo. if dict.has_key('__ac_permissions__'): for item in dict['__ac_permissions__']: permission_name = item[0] self._setaccess(item[1], permission_name) if len(item) > 2: self.setPermissionDefault(permission_name, item[2]) # Set __roles__ for attributes declared public or private. # Collect protected attribute names in ac_permissions. ac_permissions = {} for name, access in self.names.items(): if access in (ACCESS_PRIVATE, ACCESS_PUBLIC, ACCESS_NONE): setattr(classobj, '%s__roles__' % name, access) else: if not ac_permissions.has_key(access): ac_permissions[access] = [] ac_permissions[access].append(name) # Now transform our nested dict structure into the nested tuple # structure expected of __ac_permissions__ attributes and set # it on the class object. getRoles = self.roles.get __ac_permissions__ = [] permissions = ac_permissions.items() permissions.sort() for permission_name, names in permissions: roles = getRoles(permission_name, ()) if len(roles): entry = (permission_name, tuple(names), tuple(roles.keys())) else: entry = (permission_name, tuple(names)) __ac_permissions__.append(entry) setattr(classobj, '__ac_permissions__', tuple(__ac_permissions__)) # Take care of default attribute access policy access = getattr(self, 'access', _marker) if access is not _marker: setattr(classobj, '__allow_access_to_unprotected_subobjects__', access) if getattr(self, '_warnings', None): LOG('SecurityInfo', WARNING, 'Class "%s" had conflicting ' 'security declarations' % classobj.__name__) class ClassSecurityInformation(ClassSecurityInfo): # Default policy is disallow access = 0 _moduleSecurity = {} _appliedModuleSecurity = {} def secureModule(mname, *imp): modsec = _moduleSecurity.get(mname, None) if modsec is None: return del _moduleSecurity[mname] if imp: __import__(mname, *imp) module = sys.modules[mname] modsec.apply(module.__dict__) _appliedModuleSecurity[mname] = modsec return module def ModuleSecurityInfo(module_name=None): if module_name is not None: modsec = _moduleSecurity.get(module_name, None) if modsec is not None: return modsec modsec = _appliedModuleSecurity.get(module_name, None) if modsec is not None: # Move security info back to to-apply dict (needed for product # refresh). Also invoke this check for parent packages already # applied del _appliedModuleSecurity[module_name] _moduleSecurity[module_name] = modsec dot = module_name.rfind('.') if dot > 0: ModuleSecurityInfo(module_name[:dot]) return modsec dot = module_name.rfind('.') if dot > 0: # If the module is in a package, recursively make sure # there are security declarations for the package steps # leading to the module modname = module_name[dot + 1:] pmodsec = ModuleSecurityInfo(module_name[:dot]) if not pmodsec.names.has_key(modname): pmodsec.declarePublic(modname) return _ModuleSecurityInfo(module_name) class _ModuleSecurityInfo(SecurityInfo): """Encapsulate security information for modules.""" __roles__ = ACCESS_PRIVATE def __init__(self, module_name=None): self.names = {} if module_name is not None: global _moduleSecurity _moduleSecurity[module_name] = self __call____roles__ = ACCESS_PRIVATE def __call__(self, name, value): """Callback for __allow_access_to_unprotected_subobjects__ hook.""" access = self.names.get(name, _marker) if access is not _marker: return access == ACCESS_PUBLIC return getattr(self, 'access', 0) apply__roles__ = ACCESS_PRIVATE def apply(self, dict): """Apply security information to the given module dict.""" # Start with default attribute access policy access = getattr(self, 'access', _marker) if access is not _marker or len(self.names): dict['__allow_access_to_unprotected_subobjects__'] = self if getattr(self, '_warnings', None): LOG('SecurityInfo', WARNING, 'Module "%s" had conflicting ' 'security declarations' % dict['__name__']) declareProtected__roles__=ACCESS_PRIVATE def declareProtected(self, permission_name, *names): """Cannot declare module names protected.""" pass declareObjectProtected__roles__=ACCESS_PRIVATE def declareObjectProtected(self, permission_name): """Cannot declare module protected.""" pass setDefaultRoles__roles__=ACCESS_PRIVATE def setDefaultRoles(self, permission_name, roles): """Cannot set default roles for permissions in a module.""" pass # Handy little utility functions def allow_module(module_name): """Allow a module and all its contents to be used from a restricted Script. The argument module_name may be a simple or dotted module or package name. Note that if a package path is given, all modules in the path will be available.""" ModuleSecurityInfo(module_name).setDefaultAccess(1) dot = module_name.find('.') while dot > 0: ModuleSecurityInfo(module_name[:dot]).setDefaultAccess(1) dot = module_name.find('.', dot + 1) def allow_class(Class): """Allow a class and all of its methods to be used from a restricted Script. The argument Class must be a class.""" Class._security = sec = ClassSecurityInfo() sec.declareObjectPublic() sec.setDefaultAccess(1) sec.apply(Class) from Globals import InitializeClass InitializeClass(Class)