191 lines
6.4 KiB
Python
191 lines
6.4 KiB
Python
# Copyright 2011 Dustin C. Hatch
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
'''Convenient decorators for enforcing authorization on controllers
|
|
|
|
:Created: Mar 3, 2011
|
|
:Author: dustin
|
|
'''
|
|
|
|
from functools import wraps
|
|
from milla.auth import permissions
|
|
import milla.auth
|
|
import warnings
|
|
try:
|
|
import pkg_resources
|
|
except ImportError:
|
|
pkg_resources = None
|
|
|
|
|
|
__all__ = [
|
|
'auth_required',
|
|
'require_perms',
|
|
'validate_request',
|
|
]
|
|
|
|
|
|
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):
|
|
warnings.warn(
|
|
'_validate_request is deprecated; use validate_request instead',
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
validate_request(func, requirement, *args, **kwargs)
|
|
|
|
|
|
def validate_request(func, requirement, *args, **kwargs):
|
|
'''Validate a request meets a given requirement
|
|
|
|
:param func: Decorated callable
|
|
:param requirement: A requirement that the request must meet in
|
|
order to be considered valid, as specified by the request
|
|
validator used by the application. This is usally a sub-class of
|
|
:py:class:`~milla.auth.permissions.PermissionRequirement`, or
|
|
some other class that has a ``check`` method that accepts a
|
|
:py:class:`~milla.Request` object as its only argument.
|
|
:param args: Positional arguments to pass through to the decorated
|
|
callable
|
|
:param kwargs: Keyword arguments to pass through to the decorated
|
|
callable
|
|
|
|
This is a helper function used by :py:func:`auth_required` and
|
|
:py:func:`require_perms` that can be used by other request
|
|
decorators as well.
|
|
'''
|
|
request = _find_request(*args, **kwargs)
|
|
|
|
rv = request.config.get('request_validator', 'default')
|
|
if hasattr(rv, 'validate'):
|
|
# Config specifies a request validator class explicitly instead
|
|
# of an entry point name, so use it directly
|
|
validator = rv()
|
|
elif pkg_resources:
|
|
for ep in pkg_resources.iter_entry_points(VALIDATOR_EP_GROUP, rv):
|
|
try:
|
|
validator = ep.load()()
|
|
break
|
|
except:
|
|
# Ignore errors loading entry points or creating instances
|
|
continue
|
|
else:
|
|
# No entry point loaded or request validator instance
|
|
# created, use the default
|
|
validator = milla.auth.RequestValidator()
|
|
else:
|
|
# config does not specify a request validator class, and
|
|
# setuptools is not available, use the default
|
|
validator = milla.auth.RequestValidator()
|
|
|
|
try:
|
|
validator.validate(request, requirement)
|
|
except milla.auth.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 not hasattr(req, 'check'):
|
|
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
|