Imported authentication-related features from Thunder
parent
303b0ff880
commit
f3b0f44449
5
setup.py
5
setup.py
|
@ -18,4 +18,9 @@ setup(
|
||||||
],
|
],
|
||||||
packages=find_packages('src', exclude=['distribute_setup']),
|
packages=find_packages('src', exclude=['distribute_setup']),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
|
entry_points={
|
||||||
|
'milla.request_validator': [
|
||||||
|
'default = milla.auth:RequestValidator'
|
||||||
|
]
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,3 +5,4 @@ from app import *
|
||||||
from webob.exc import *
|
from webob.exc import *
|
||||||
from webob.request import *
|
from webob.request import *
|
||||||
from webob.response import *
|
from webob.response import *
|
||||||
|
from auth.decorators import *
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
'''Request authorization
|
||||||
|
|
||||||
|
:Created: Apr 5, 2011
|
||||||
|
:Author: dustin
|
||||||
|
:Updated: $Date$
|
||||||
|
:Updater: $Author$
|
||||||
|
'''
|
||||||
|
|
||||||
|
class NotAuthorized(Exception):
|
||||||
|
'''Base class for unauthorized exceptions
|
||||||
|
|
||||||
|
This class is both an exception and a controller callable. If the
|
||||||
|
request validator raises an instance of this class, it will be
|
||||||
|
called and the resulting value will become the HTTP response. The
|
||||||
|
default implementation simply returns HTTP status 403 and a simple
|
||||||
|
body containing the exception message.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __call__(self, request, *args, **kwargs):
|
||||||
|
'''Return a response indicating the request is not authorized
|
||||||
|
|
||||||
|
:param request: WebOb Request instance for the current request
|
||||||
|
|
||||||
|
All other arguments and keywords are ignored.
|
||||||
|
'''
|
||||||
|
|
||||||
|
response = request.ResponseClass(unicode(self))
|
||||||
|
response.status_int = 403
|
||||||
|
return response
|
||||||
|
|
||||||
|
class RequestValidator(object):
|
||||||
|
'''Base class for request validators
|
||||||
|
|
||||||
|
A request validator is a class that exposes a ``validate`` method,
|
||||||
|
which accepts an instance of :py:class:`webob.Request` and an
|
||||||
|
optional ``requirement``. The ``validate`` method should return
|
||||||
|
``None`` on successful validation, or raise an instance of
|
||||||
|
:py:exc:`NotAuthorized` on failure. The base implementation will
|
||||||
|
raise an instance of the exception specified by
|
||||||
|
:py:attr:`exc_class`, which defaults to :py:class`NotAuthorized`.
|
||||||
|
|
||||||
|
To customize the response to unauthorized requests, it is
|
||||||
|
sufficient to subclass :py:class:`NotAuthorized`, override its
|
||||||
|
:py:meth:`~NotAuthorized.__call__` method, and specify the class
|
||||||
|
in :py:attr:`exc_class`.
|
||||||
|
'''
|
||||||
|
|
||||||
|
#: Exception class to raise if the request is unauthorized
|
||||||
|
exc_class = NotAuthorized
|
||||||
|
|
||||||
|
def validate(self, request, requirement=None):
|
||||||
|
'''Validates a request
|
||||||
|
|
||||||
|
:param request: The request to validate. Should be an instance
|
||||||
|
of :py:class:`webob.Request`.
|
||||||
|
:param requirement: (Optional) A requirement to check. Should be
|
||||||
|
an instance of :py:class:`~milla.auth.permissions.Permission`
|
||||||
|
or :py:class:`~milla.auth.permissions.PermissionRequirement`,
|
||||||
|
or some other class with a ``check`` method that accepts a
|
||||||
|
sequence of permissions.
|
||||||
|
|
||||||
|
The base implementation will perform authorization in the
|
||||||
|
following way:
|
||||||
|
|
||||||
|
1. Does the ``request`` have a ``user`` attribute? If not,
|
||||||
|
raise :py:exc:`NotAuthorized`.
|
||||||
|
2. Is the truth value of ``request.user`` true? If not, raise
|
||||||
|
:py:exc:`NotAuthorized`.
|
||||||
|
3. Does the ``request.user`` object have a ``permissions``
|
||||||
|
attribute? If not, raise :py:exc:`NotAuthorized`.
|
||||||
|
4. Do the user's permissions meet the requirements? If not,
|
||||||
|
raise :py:exc:`NotAuthorized`.
|
||||||
|
|
||||||
|
If none of the above steps raised an exception, the method will
|
||||||
|
return ``None``, indicating that the validation was successful.
|
||||||
|
|
||||||
|
.. note:: WebOb Request instances do not have a ``user``
|
||||||
|
attribute by default. You will need to supply this yourself,
|
||||||
|
i.e. in a WSGI middleware or in the ``__before__`` method of
|
||||||
|
your controller class.
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = request.user
|
||||||
|
except AttributeError:
|
||||||
|
# No user associated with the request at all
|
||||||
|
raise self.exc_class('Request has no user')
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise self.exc_class('Anonymous not allowed')
|
||||||
|
|
||||||
|
if requirement:
|
||||||
|
try:
|
||||||
|
user_perms = user.permissions
|
||||||
|
except AttributeError:
|
||||||
|
raise self.exc_class('User has no permissions')
|
||||||
|
|
||||||
|
if not requirement.check(user_perms):
|
||||||
|
raise self.exc_class('User does not have required permissions')
|
|
@ -0,0 +1,126 @@
|
||||||
|
'''Convenient decorators for enforcing authorization on controllers
|
||||||
|
|
||||||
|
:Created: Mar 3, 2011
|
||||||
|
:Author: dustin
|
||||||
|
:Updated: $Date$
|
||||||
|
:Updater: $Author$
|
||||||
|
'''
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
from milla.auth import RequestValidator, NotAuthorized, permissions
|
||||||
|
import milla
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
__all__ = ['auth_required', 'require_perms']
|
||||||
|
|
||||||
|
VALIDATOR_EP_GROUP = 'milla.request_validator'
|
||||||
|
|
||||||
|
def _find_request(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return kwargs['request']
|
||||||
|
except KeyError:
|
||||||
|
for arg in args:
|
||||||
|
if isinstance(arg, milla.Request):
|
||||||
|
return arg
|
||||||
|
|
||||||
|
def _validate_request(func, requirement, *args, **kwargs):
|
||||||
|
request = _find_request(*args, **kwargs)
|
||||||
|
ep_name = request.config.get('request_validator', 'default')
|
||||||
|
|
||||||
|
# Override the RequestVariable name with a class from the specified
|
||||||
|
# entry point, if one is available. Otherwise, the default is used.
|
||||||
|
for ep in pkg_resources.iter_entry_points(VALIDATOR_EP_GROUP, ep_name):
|
||||||
|
try:
|
||||||
|
RequestValidator = ep.load()
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
validator = RequestValidator()
|
||||||
|
validator.validate(request, requirement)
|
||||||
|
except NotAuthorized as e:
|
||||||
|
return e(request)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
def auth_required(func):
|
||||||
|
'''Simple decorator to enforce authentication for a controller
|
||||||
|
|
||||||
|
Example usage::
|
||||||
|
|
||||||
|
class SomeController(object):
|
||||||
|
|
||||||
|
def __before__(request):
|
||||||
|
request.user = find_a_user_somehow(request)
|
||||||
|
|
||||||
|
@milla.auth_required
|
||||||
|
def __call__(request):
|
||||||
|
return 'Hello, world!'
|
||||||
|
|
||||||
|
In this example, the ``SomeController`` controller class implements
|
||||||
|
an ``__before__`` method that adds the ``user`` attribute to the
|
||||||
|
``request`` instance. This could be done by extracting user
|
||||||
|
information from the HTTP session, for example. The ``__call__``
|
||||||
|
method is decorated with ``auth_required``, which will ensure that
|
||||||
|
the user is successfully authenticated. This is handled by a
|
||||||
|
*request validator*.
|
||||||
|
|
||||||
|
If the request is not authorized, the decorated method will never
|
||||||
|
be called. Instead, the response is generated by calling the
|
||||||
|
:py:exc:`~milla.auth.NotAuthorized` exception raised inside
|
||||||
|
the ``auth_required`` decorator.
|
||||||
|
'''
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return _validate_request(func, None, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class require_perms(object):
|
||||||
|
'''Decorator that requires the user have certain permissions
|
||||||
|
|
||||||
|
Example usage::
|
||||||
|
|
||||||
|
class SomeController(object):
|
||||||
|
|
||||||
|
def __before__(request):
|
||||||
|
request.user = find_a_user_somehow(request)
|
||||||
|
|
||||||
|
@milla.require_perms('some_permission', 'and_this_permission')
|
||||||
|
def __call__(request):
|
||||||
|
return 'Hello, world!'
|
||||||
|
|
||||||
|
In this example, the ``SomeController`` controller class implements
|
||||||
|
an ``__before__`` method that adds the ``user`` attribute to the
|
||||||
|
``request`` instance. This could be done by extracting user
|
||||||
|
information from the HTTP session, for example. The ``__call__``
|
||||||
|
method is decorated with ``require_perms``, which will ensure that
|
||||||
|
the user is successfully authenticated and the the user has the
|
||||||
|
specified permissions. This is handled by a *request validator*.
|
||||||
|
|
||||||
|
There are two ways to specify the required permissions:
|
||||||
|
|
||||||
|
* By passing the string name of all required permissions as
|
||||||
|
positional arguments. A complex permission requirement will be
|
||||||
|
constructed that requires *all* of the given permissions to be
|
||||||
|
held by the user in order to validate
|
||||||
|
* By explicitly passing an instance of
|
||||||
|
:py:class:`~milla.auth.permissions.Permission` or
|
||||||
|
:py:class:`~milla.auth.permissions.PermissionRequirement`
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, *requirements):
|
||||||
|
requirement = None
|
||||||
|
for req in requirements:
|
||||||
|
if isinstance(req, basestring):
|
||||||
|
req = permissions.Permission(req)
|
||||||
|
if not requirement:
|
||||||
|
requirement = req
|
||||||
|
else:
|
||||||
|
requirement &= req
|
||||||
|
self.requirement = requirement
|
||||||
|
|
||||||
|
def __call__(self, func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return _validate_request(func, self.requirement, *args, **kwargs)
|
||||||
|
return wrapper
|
|
@ -0,0 +1,127 @@
|
||||||
|
'''Classes for calculating user permissions
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
>>> req = Permission('foo') & Permission('bar')
|
||||||
|
>>> req.check(PermissionContainer(['foo', 'baz'], ['bar']))
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> req = Permission('login')
|
||||||
|
>>> req.check(['login'])
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> req = Permission('login') | Permission('admin')
|
||||||
|
>>> req.check(['none'])
|
||||||
|
False
|
||||||
|
'''
|
||||||
|
|
||||||
|
class PermissionContainer(object):
|
||||||
|
'''Container object for user and group permissions
|
||||||
|
|
||||||
|
:param list user_perms: List of permissions held by the user itself
|
||||||
|
:param list group_perms: List of permissions held by the groups to
|
||||||
|
which the user belongs
|
||||||
|
|
||||||
|
Iterating over :py:class:`PermissionContainer` objects results in
|
||||||
|
a flattened representation of all permissions.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, user_perms=[], group_perms=[]):
|
||||||
|
self._user_perms = user_perms
|
||||||
|
self._group_perms = group_perms
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for perm in self._user_perms:
|
||||||
|
yield perm
|
||||||
|
for perm in self._group_perms:
|
||||||
|
yield perm
|
||||||
|
|
||||||
|
def __in__(self, perm):
|
||||||
|
return perm in self._user_perms or perm in self._group_perms
|
||||||
|
|
||||||
|
|
||||||
|
class BasePermission(object):
|
||||||
|
'''Base class for permissions and requirements
|
||||||
|
|
||||||
|
Complex permission requirements can be created using the bitwise
|
||||||
|
``and`` and ``or`` operators::
|
||||||
|
|
||||||
|
login_and_view = Permission('login') & Permission('view')
|
||||||
|
admin_or_root = Permission('admin') | Permission('root')
|
||||||
|
|
||||||
|
complex = Permission('login') & Permission('view') | Permission('admin')
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __and__(self, other):
|
||||||
|
assert isinstance(other, BasePermission)
|
||||||
|
return PermissionRequirementAll(self, other)
|
||||||
|
|
||||||
|
def __or__(self, other):
|
||||||
|
assert isinstance(other, BasePermission)
|
||||||
|
return PermissionRequirementAny(self, other)
|
||||||
|
|
||||||
|
class Permission(BasePermission):
|
||||||
|
'''Simple permission implementation
|
||||||
|
|
||||||
|
:param str name: Name of the permission
|
||||||
|
|
||||||
|
Permissions must implement a ``check`` method that accepts an
|
||||||
|
iterable and returns ``True`` if the permission is present or
|
||||||
|
``False`` otherwise.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
if isinstance(self.name, unicode):
|
||||||
|
return self.name
|
||||||
|
else:
|
||||||
|
return self.name.decode('utf-8')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if isinstance(self.name, str):
|
||||||
|
return self.name
|
||||||
|
else:
|
||||||
|
return self.name.encode('utf-8')
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self is other or str(self) == str(other)
|
||||||
|
|
||||||
|
def check(self, perms):
|
||||||
|
'''Check if the permission is held
|
||||||
|
|
||||||
|
This method can be overridden to provide more robust
|
||||||
|
support, but this implementation is simple::
|
||||||
|
|
||||||
|
return self in perms
|
||||||
|
'''
|
||||||
|
|
||||||
|
return self in perms
|
||||||
|
|
||||||
|
class PermissionRequirement(BasePermission):
|
||||||
|
'''Base class for complex permission requirements'''
|
||||||
|
|
||||||
|
def __init__(self, *requirements):
|
||||||
|
self.requirements = requirements
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return unicode(self).encode('utf-8')
|
||||||
|
|
||||||
|
class PermissionRequirementAll(PermissionRequirement):
|
||||||
|
'''Complex permission requirement needing all given permissions'''
|
||||||
|
|
||||||
|
def check(self, perms):
|
||||||
|
for req in self.requirements:
|
||||||
|
if not req.check(perms):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
class PermissionRequirementAny(PermissionRequirement):
|
||||||
|
'''Complex permission requirement needing any given permissions'''
|
||||||
|
|
||||||
|
def check(self, perms):
|
||||||
|
for req in self.requirements:
|
||||||
|
if req.check(perms):
|
||||||
|
return True
|
||||||
|
return False
|
Loading…
Reference in New Issue