pylons.decorators.secure
Covered: 80 lines
Missed: 0 lines
Skipped 31 lines
Percent: 100 %
  1
"""Security related decorators"""
  2
import logging
  3
import urlparse
  5
from decorator import decorator
  6
try:
  7
    import webhelpers.html.secure_form as secure_form
  8
except ImportError:
  9
    import webhelpers.pylonslib.secure_form as secure_form
 11
from pylons.controllers.util import abort, redirect
 12
from pylons.decorators.util import get_pylons
 14
__all__ = ['authenticate_form', 'https']
 16
log = logging.getLogger(__name__)
 18
csrf_detected_message = (
 19
    "Cross-site request forgery detected, request denied. See "
 20
    "http://en.wikipedia.org/wiki/Cross-site_request_forgery for more "
 21
    "information.")
 23
def authenticated_form(params):
 24
    submitted_token = params.get(secure_form.token_key)
 25
    return submitted_token is not None and \
 26
        submitted_token == secure_form.authentication_token()
 29
@decorator
 30
def authenticate_form(func, *args, **kwargs):
 31
    """Decorator for authenticating a form
 33
    This decorator uses an authorization token stored in the client's
 34
    session for prevention of certain Cross-site request forgery (CSRF)
 35
    attacks (See
 36
    http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
 37
    information).
 39
    For use with the ``webhelpers.html.secure_form`` helper functions.
 41
    """
 42
    request = get_pylons(args).request
 43
    if authenticated_form(request.POST):
 44
        del request.POST[secure_form.token_key]
 45
        return func(*args, **kwargs)
 46
    else:
 47
        log.warn('Cross-site request forgery detected, request denied: %r '
 48
                 'REMOTE_ADDR: %s' % (request, request.remote_addr))
 49
        abort(403, detail=csrf_detected_message)
 52
def https(url_or_callable=None):
 53
    """Decorator to redirect to the SSL version of a page if not
 54
    currently using HTTPS. Apply this decorator to controller methods
 55
    (actions).
 57
    Takes a url argument: either a string url, or a callable returning a
 58
    string url. The callable will be called with no arguments when the
 59
    decorated method is called. The url's scheme will be rewritten to
 60
    https if necessary.
 62
    Non-HTTPS POST requests are aborted (405 response code) by this
 63
    decorator.
 65
    Example:
 67
    .. code-block:: python
 70
        @https('/pylons')
 71
        def index(self):
 72
            do_secure()
 77
        @https(lambda: url(controller='auth', action='login'))
 78
        def login(self):
 79
            do_secure()
 82
        @https()
 83
        def get(self):
 84
            do_secure()
 86
    """
 87
    def wrapper(func, *args, **kwargs):
 88
        """Decorator Wrapper function"""
 89
        request = get_pylons(args).request
 90
        if request.scheme.lower() == 'https':
 91
            return func(*args, **kwargs)
 92
        if request.method.upper() == 'POST':
 94
            abort(405, headers=[('Allow', 'GET')])
 96
        if url_or_callable is None:
 97
            url = request.url
 98
        elif callable(url_or_callable):
 99
            url = url_or_callable()
100
        else:
101
            url = url_or_callable
103
        parts = urlparse.urlparse(url)
104
        url = urlparse.urlunparse(('https', parts[1] or request.host) +
105
                                  parts[2:])
107
        log.debug('Redirecting non-https request: %s to: %s',
108
                  request.path_info, url)
109
        redirect(url)
110
    return decorator(wrapper)