Files
deb-python-pecan/pecan/secure.py
Mark McClain e4d61c882a wraps methods before applying security in SecurceController
Resolves issue #131

Patchset updates the metaclass to wrap unbound methods before adding
security.  Wrapping ensures that base controllers can safely be mixed in
both secure and unsecure contexts.  Wrapping resolves the issue since
method attributes are shared by all classes with the same bases.  The
original _pecan attributes of the method are shallow copied prior to adding the
security attributes.
2012-12-07 00:04:10 -05:00

208 lines
6.6 KiB
Python

from functools import wraps
from inspect import getmembers, ismethod, isfunction
from webob import exc
from decorators import expose
from util import _cfg, iscontroller
__all__ = ['unlocked', 'secure', 'SecureController']
class _SecureState(object):
def __init__(self, desc, boolean_value):
self.description = desc
self.boolean_value = boolean_value
def __repr__(self):
return '<SecureState %s>' % self.description
def __nonzero__(self):
return self.boolean_value
Any = _SecureState('Any', False)
Protected = _SecureState('Protected', True)
# security method decorators
def _unlocked_method(func):
_cfg(func)['secured'] = Any
return func
def _secure_method(check_permissions_func):
def wrap(func):
cfg = _cfg(func)
cfg['secured'] = Protected
cfg['check_permissions'] = check_permissions_func
return func
return wrap
# classes to assist with wrapping attributes
class _UnlockedAttribute(object):
def __init__(self, obj):
self.obj = obj
@_unlocked_method
@expose()
def _lookup(self, *remainder):
return self.obj, remainder
class _SecuredAttribute(object):
def __init__(self, obj, check_permissions):
self.obj = obj
self.check_permissions = check_permissions
self._parent = None
def _check_permissions(self):
if isinstance(self.check_permissions, basestring):
return getattr(self.parent, self.check_permissions)()
else:
return self.check_permissions()
def __get_parent(self):
return self._parent
def __set_parent(self, parent):
if ismethod(parent):
self._parent = parent.im_self
else:
self._parent = parent
parent = property(__get_parent, __set_parent)
@_secure_method('_check_permissions')
@expose()
def _lookup(self, *remainder):
return self.obj, remainder
# helper for secure decorator
def _allowed_check_permissions_types(x):
return (
ismethod(x) or
isfunction(x) or
isinstance(x, basestring)
)
# methods that can either decorate functions or wrap classes
# these should be the main methods used for securing or unlocking
def unlocked(func_or_obj):
"""
This method unlocks method or class attribute on a SecureController. Can
be used to decorate or wrap an attribute
"""
if ismethod(func_or_obj) or isfunction(func_or_obj):
return _unlocked_method(func_or_obj)
else:
return _UnlockedAttribute(func_or_obj)
def secure(func_or_obj, check_permissions_for_obj=None):
"""
This method secures a method or class depending on invocation.
To decorate a method use one argument:
@secure(<check_permissions_method>)
To secure a class, invoke with two arguments:
secure(<obj instance>, <check_permissions_method>)
"""
if _allowed_check_permissions_types(func_or_obj):
return _secure_method(func_or_obj)
else:
if not _allowed_check_permissions_types(check_permissions_for_obj):
msg = "When securing an object, secure() requires the " + \
"second argument to be method"
raise TypeError(msg)
return _SecuredAttribute(func_or_obj, check_permissions_for_obj)
class SecureController(object):
"""
Used to apply security to a controller.
Implementations of SecureController should extend the
`check_permissions` method to return a True or False
value (depending on whether or not the user has permissions
to the controller).
"""
class __metaclass__(type):
def __init__(cls, name, bases, dict_):
cls._pecan = dict(
secured=Protected,
check_permissions=cls.check_permissions,
unlocked=[]
)
for name, value in getmembers(cls)[:]:
if ismethod(value):
if iscontroller(value) and value._pecan.get(
'secured'
) is None:
# Wrap the function so that the security context is
# local to this class definition. This works around
# the fact that unbound method attributes are shared
# across classes with the same bases.
wrapped = _make_wrapper(value)
wrapped._pecan['secured'] = Protected
wrapped._pecan['check_permissions'] = \
cls.check_permissions
setattr(cls, name, wrapped)
elif hasattr(value, '__class__'):
if name.startswith('__') and name.endswith('__'):
continue
if isinstance(value, _UnlockedAttribute):
# mark it as unlocked and remove wrapper
cls._pecan['unlocked'].append(value.obj)
setattr(cls, name, value.obj)
elif isinstance(value, _SecuredAttribute):
# The user has specified a different check_permissions
# than the class level version. As far as the class
# is concerned, this method is unlocked because
# it is using a check_permissions function embedded in
# the _SecuredAttribute wrapper
cls._pecan['unlocked'].append(value)
@classmethod
def check_permissions(cls):
return False
def _make_wrapper(f):
"""return a wrapped function with a copy of the _pecan context"""
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
wrapper._pecan = f._pecan.copy()
return wrapper
# methods to evaluate security during routing
def handle_security(controller):
""" Checks the security of a controller. """
if controller._pecan.get('secured', False):
check_permissions = controller._pecan['check_permissions']
if isinstance(check_permissions, basestring):
check_permissions = getattr(controller.im_self, check_permissions)
if not check_permissions():
raise exc.HTTPUnauthorized
def cross_boundary(prev_obj, obj):
""" Check permissions as we move between object instances. """
if prev_obj is None:
return
if isinstance(obj, _SecuredAttribute):
# a secure attribute can live in unsecure class so we have to set
# while we walk the route
obj.parent = prev_obj
if hasattr(prev_obj, '_pecan'):
if obj not in prev_obj._pecan.get('unlocked', []):
handle_security(prev_obj)