Files
deb-python-pecan/pecan/secure.py
Julien Danjou f4d923dca6 Fix broken wsme-stable job and pep8 failures.
Change-Id: I4ff0a7a7926f7a645fa8d59242be9d31bd683106
2015-03-02 12:29:37 -05:00

233 lines
7.1 KiB
Python

from functools import wraps
from inspect import getmembers, isfunction
from webob import exc
import six
from .decorators import expose
from .util import _cfg, iscontroller
__all__ = ['unlocked', 'secure', 'SecureController']
if six.PY3:
from .compat import is_bound_method as ismethod
else:
from inspect import ismethod
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
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(<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 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)