Protecting controllers and controller actions

Overview

This plugin provides decorators to add access rules to controllers and controller actions, which have been created with extensibility in mind so that you can adapt them to suit your needs.

Protecting a controller action

To set controller-wide access rules, you can use the ActionProtector class decorator as in the example below:

from repoze.what.predicates import has_permission
from repoze.what.plugins.pylonshq import ActionProtector

class RootController(YourBaseController):

    # Place an expose() here if you use TG2
    def index(self):
        return nice_function_to_do_nothing()

    # Place an expose() here if you use TG2
    @ActionProtector(has_permission('edit-articles'))
    def edit_article(self, article_id):
        return get_article_edition_form(article_id)

With the controller and action controllers above, anyone who tries to access /edit_article will have to be granted the edit-articles permission. Otherwise, authorization will be denied.

Tip

TurboGears 2 provides the tg.require decorator to set the access rules in controller actions, which is a subclass of ActionProtector with additional functionality specific to TG2 applications.

Protecting a controller

To set controller-wide access rules, you can use the ControllerProtector class decorator as in the example below:

from repoze.what.predicates import has_permission
from repoze.what.plugins.pylonshq import ControllerProtector

@ControllerProtector(has_permission('manage'))
class ControlPanel(YourBaseController):
    # Place an expose() here if you use TG2
    def index(self):
        return nice_function_to_do_nothing()

    # Place an expose() here if you use TG2
    def delete_user(self, user_id):
        return nice_function_to_delete_a_user(user_id)

class RootController(YourBaseController):

    panel = ControlPanel()

    # Place an expose() here if you use TG2
    def index(self):
        return nice_function_to_do_nothing()

With the controllers and action controllers above, anyone who tries to access /panel/* will have to be granted the manage permission. Otherwise, authorization will be denied.

Tip

As of version 2.0b6, TurboGears provides the tg.allow_only decorator for controller-wide authorization, which is a subclass of ControllerProtector with additional functionality specific to TG2 applications.

Note

If you’re using Python v2.4 or v2.5, you will have to use the alternate syntax because class decorators are supported as of Python v2.6:

class ControlPanel(YourBaseController):
    # ...
    pass
ControlPanel = ControllerProtector(has_permission('manage'))(ControlPanel)

Using denial handlers

By default, an authorization denial triggers one of the following actions:

  • If the user is anonymous, repoze.who will perform a challenge (e.g., a login form will be displayed).
  • If the user is authenticated, a page whose HTTP status code is 403 will be served.

If you want to override the default behavior when authorization is denied, you have define a so-called “denial handler”. A denial handler is a callable which receives one positional argument (which is the message that describes why authorization is denied; this is, the relevant repoze.what predicate message) and is called only when authorization is denied.

The following is a denial handler:

# This is yourapplication.anotherpackage

from pylons import request, response
from pylons.controllers.util import abort
# nice_flash is a function that inserts a user-visible message in the
# template
from yourapplication.somepackage import nice_flash

def cool_denial_handler(reason):
    # When this handler is called, response.status has two possible values:
    # 401 or 403.
    if response.status_int == 401:
        message = 'Oops, you have to login: %s' % reason
        message_type = 'warning'
    else:
        identity = request.environ['repoze.who.identity']
        userid = identity['repoze.who.userid']
        message = "Come on, %s, you know you can't do that: %s" % (userid,
                                                                   reason)
        message_type = 'error'
    nice_flash(message, message_type)
    abort(response.status_int, comment=reason)

Attention

The denial handler above must call abort(), otherwise we’d be granting access to the request denied by repoze.what. Note that this is a feature, not a bug: In some situations you may not want to abort (e.g., you may want to redirect).

And you can use it as in:

from repoze.what.predicates import has_permission
from repoze.what.plugins.pylonshq import ActionProtector, ControllerProtector

from yourapplication.anotherpackage import cool_denial_handler

@ControllerProtector(has_permission('manage'), cool_denial_handler)
class ControlPanel(YourBaseController):
    # Place an expose() here if you use TG2
    def index(self):
        return nice_function_to_do_nothing()

    # Place an expose() here if you use TG2
    def delete_user(self, user_id):
        return nice_function_to_delete_a_user(user_id)

class RootController(YourBaseController):

    panel = ControlPanel()

    # Place an expose() here if you use TG2
    def index(self):
        return nice_function_to_do_nothing()

    # Place an expose() here if you use TG2
    @ActionProtector(has_permission('edit-articles'), cool_denial_handler)
    def edit_article(self, article_id):
        return get_article_edition_form(article_id)

Then, when authorization is denied:

  • If the user is anonymous, she should be served a web page which contains a login form and a message that starts with “Oops, you have to login (...)”. The status code of such a response is up to the repoze.who challenger.
  • If the user is authenticated, she should be served a web page that contains a message that starts with “Come on, {{username}}, you know (..)” and whose HTTP status code is 403.

Creating application-specific protectors

Sometimes you may need to customize the controller and controller action protectors in many places within your application (or in the whole application). All you have to do is subclass the relevant protector.

For example, if we use the cool_denial_handler function above very often, then we should create controller and controller action protectors which use that handler by default:

# This is yourapplication.yetanotherpackage

from repoze.what.plugins.pylonshq import ActionProtector, ControllerProtector
from yourapplication.anotherpackage import cool_denial_handler

class CoolActionProtector(ActionProtector):
    default_denial_handler = staticmethod(cool_denial_handler)

class CoolControllerProtector(ControllerProtector):
    protector = CoolActionProtector

# The following is an alternate way to define CoolControllerProtector:
# class CoolControllerProtector(ControllerProtector):
#     default_denial_handler = staticmethod(cool_denial_handler)

Then our controllers would look like this:

from repoze.what.predicates import has_permission

from yourapplication.yetanotherpackage import CoolActionProtector, \
                                              CoolControllerProtector

@CoolControllerProtector(has_permission('manage'))
class ControlPanel(YourBaseController):
    # Place an expose() here if you use TG2
    def index(self):
        return nice_function_to_do_nothing()

    # Place an expose() here if you use TG2
    def delete_user(self, user_id):
        return nice_function_to_delete_a_user(user_id)

class RootController(YourBaseController):

    panel = ControlPanel()

    # Place an expose() here if you use TG2
    def index(self):
        return nice_function_to_do_nothing()

    # Place an expose() here if you use TG2
    @CoolActionProtector(has_permission('edit-articles'))
    def edit_article(self, article_id):
        return get_article_edition_form(article_id)

And every time authorization is denied, the cool_denial_handler function will be called.