Imported authentication-related features from Thunder

master
Dustin C. Hatch 2011-04-05 23:35:48 -05:00
parent 303b0ff880
commit f3b0f44449
5 changed files with 358 additions and 0 deletions

View File

@ -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'
]
}
) )

View File

@ -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 *

View File

@ -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')

View File

@ -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

View File

@ -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