from functools import wraps from inspect import getmembers, isfunction from webob import exc import six if six.PY3: from .compat import is_bound_method as ismethod else: from inspect import ismethod 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 '' % self.description def __nonzero__(self): return self.boolean_value def __bool__(self): return self.__nonzero__() 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, six.string_types): 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 = six.get_method_self(parent) 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, six.string_types) ) # 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() To secure a class, invoke with two arguments: secure(, ) """ 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 SecureControllerMeta(type): """ 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). """ def __init__(cls, name, bases, dict_): cls._pecan = dict( secured=Protected, check_permissions=cls.check_permissions, unlocked=[] ) for name, value in getmembers(cls)[:]: if (isfunction if six.PY3 else 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) class SecureControllerBase(object): @classmethod def check_permissions(cls): """ Returns `True` or `False` to grant access. Implemented in subclasses of :class:`SecureController`. """ return False SecureController = SecureControllerMeta( 'SecureController', (SecureControllerBase,), {'__doc__': SecureControllerMeta.__doc__} ) 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, im_self=None): """ Checks the security of a controller. """ if controller._pecan.get('secured', False): check_permissions = controller._pecan['check_permissions'] if isinstance(check_permissions, six.string_types): check_permissions = getattr( im_self or six.get_method_self(controller), 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)