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']),
|
||||
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.request 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