# 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