diff --git a/docs/creating_requirements.rst b/docs/creating_requirements.rst deleted file mode 100644 index 00a37a7..0000000 --- a/docs/creating_requirements.rst +++ /dev/null @@ -1,140 +0,0 @@ -.. _creating_requirements: - -##################### -Creating Requirements -##################### - -The core portion of Flask-Allows is made of the -:ref:`~flask_allows.requirements.Requirement` which checks to see if a given -identity and request satisfies its requirements. A requirement is any callable -that accepts both an identity and a request, and can be as simple as checking -if an identity has a certain permission:: - - class HasPermission(Requirement): - def __init__(self, permission): - self.permission = permission - - def fulfill(self, user, request): - return self.permission in user.permission - -Or as complicated as checking if the resource specified by the request is in a -certain state:: - - def user_can_access_frob(user, request): - frob_id = request.view_args.get('frob_id') - if frob_id is None: - return False - - frob = Frob.query.get_or_404(frob_id) - - return frob.is_active and frob.state == 'settled': - - -Both of these requirements can be applied to a route via a decorator:: - - from flask_allows import Allows - from myapp.requirements import HasPermission, user_can_access_frob - - allows = Allows() - - @allows.requires(HasPermission('view_frob'), user_can_access_frob) - def view_frob(frob_id): - frob = Frob.query.get_or_404(frob_id) - return render_template('frob.html', frob=frob) - -Alternatively, there is a standalone decorator that is possible to use as well:: - - from flask_allows import requires - from myapp.requirements import HasPermission, user_can_access_frob - - @requires(HasPermission('view_frob'), user_can_access_frob) - def view_frob(frob_id): - frob = Frob.query.get_or_404(frob_id) - return render_template('frob.html', frob=frob) - -.. note:: - - In order to use a requirement implemented as a class, it must be - instantiated and have the special ``__call__`` method implemented to - receive the identity and request parameters. - - Inheriting from :class:`~flask_allows.requirements.Requirement` isn't - necessary but it does mark ``fulfill`` as an abstractmethod which is - easier to remember to implement than ``__call__`` - - -By default, when multiple requirements are provided to the route decorators, -they are combined in an and fashion, meaning all of the requirements must be -fulfilled otherwise the authorization attempt is considered a failure. - -However, :class:`~flask_allows.requirements.ConditionalRequirement` provides -ways to combine requirements in other fashions as well, for example if we -would want to allow a user to access the ``view_frob`` endpoint if *either* -of the requirements were fulfilled, then it could be done like:: - - - from flask_allows import requires, Or - from myapp.requirements import HasPermission, user_can_access_frob - - @requires(Or(HasPermission('view_frob'), user_can_access_frob)) - def view_frob(frob_id): - frob = Frob.query.get_or_404(frob_id) - return render_template('frob.html', frob=frob) - -Flask-Allows provides ``Or``, ``And``, and ``Not`` helpers for combining -requirements in different ways. ``And`` is the default behavior when supplying -requirements to a route decorator, but is useful when building up a complex -requirement:: - - Or( - HasPermission('admin'), - And(HasPermission('view_frob'), user_can_access_frob) - ) - -``Not`` isn't necessarily about combining permissions, but about being able -to apply a logical not to a permission without having to change the code of -the requirement itself:: - - def user_is_active(user, request): - return user.activated - - @requires(Not(user_is_active)) - def activate_account(): - # account activation code - -If the provided helpers do not cover a use case you have, it's also possible -to create a custom requirement combinator:: - - # also available for import under its full name ConditionalRequirement - from flask_allows import C - from operator import xor - - @requires(C(HasPermission('view_frob'), user_can_access_frob, op=xor)) - def maybe_view_frob(frob_id): - pass - -This would only a user that has the ``view_frob`` permission OR if the frob -is in a particular state, but disallow access if neither OR both is true. The -``op`` argument can be any callable that can combine two booleans and returns -a boolean. - -To invert this requirement, it can either be wrapped in ``Not`` or there is a -``negated`` keyword that can passed to the -:class:`~flask_allows.requirements.ConditionalRequirement` constructor to -negate the final output:: - - C(HasPermission, user_can_access_frob, op=xor, negated=True) - -This will be considered fulfilled if both requirements are either fulfilled -or not fulfilled. - -Finally, it's possible to cause a requirement combinator to stop processing -early by providing an ``until`` to the constructor, for example the ``And`` -helper is implemented like:: - - @classmethod - def And(cls, *requirements): - return C(*requirements, until=False, op=operator.and_) - -This requirement will stop processing as soon as it hits a False and not run -any further child requirements. diff --git a/docs/failure.rst b/docs/failure.rst new file mode 100644 index 0000000..5dd8344 --- /dev/null +++ b/docs/failure.rst @@ -0,0 +1,98 @@ +.. _failure: + + +=================== +Controlling Failure +=================== + + +When dealing with permissioning, failure is an expected and desired outcome. And +Flask-Allows provides several measures to deal with this failure. + + +********************* +Throwing an exception +********************* + +The first measure is the ability to configure Requirement runners to throw an +exception. By default this will be werkzeug's Forbidden exception. However, +this can be set to be any exception type or specific instance. The easiest +way to set this is through the :class:`~flask_allows.allows.Allows` constructor:: + + class PermissionError(Exception): + def __init__(self): + super().__init__("I'm sorry Dave, I'm afraid I can't do that") + + + allows = Allows(throws=PermissionError) + + # alternatively + allows = Allows(throws=PermissionError()) + + +If a particular exception is desirable most of but not all of the time, an +exception type or instance can be provided each Requirement runner:: + + # to Permission helper + Permission(SomeRequirement(), throws=PermissionError) + + # to decorators + @allows.requires(SomeRequirement(), throws=PermissionError) + @requires(SomeRequirement(), throws=PermissionError) + + +**************** +Failure Callback +**************** + +Another way to handle failure is providing an ``on_fail`` argument that will be +invoked when failure happens. The value provided to ``on_fail`` doesn't have to +be a callable, so any value is appropriate. If the value provided is a callable +it should be prepared to accept any arbitrary arguments that were provided when +the requirement runner that was invoked. + +To add a failure callback, it can be provided to the +:class:`~flask_allows.allows.Allows` constructor:: + + def flash_failure_message(*args, **kwargs): + flash("I'm sorry Dave, I'm afraid I can't do that", "error") + + allows = Allows(on_fail=flash_failure_message) + + +If ``on_fails`` return a non-``None`` value, that will be used as the return +value from the requirement runner. However, if a ``None`` is returned from the +callback, then the configured exception is raised instead. In the above example, +since a ``None`` is implicitly returned from the callback, a werkzeug Forbidden +exception would be raised from any requirements runners. + +An example of returning a value from the callback would be returning a redirect +to another page:: + + + def redirect_to_home(*args, **kwargs): + flash("I'm sorry Dave, I'm afraid I can't do that", "error") + return redirect(url_for("index")) + +However, any value can be returned from this wrapper. + +.. note:: + + When used with the :class:`~flask_allows.permission.Permission` helper, + the callback will be invoked with no arguments and the return value isn't + considered. + +.. danger:: + + When using ``on_fail`` with route decorators, be sure to return an + appropriate value for Flask to turn into a response. + +Similar to exception configuration, ``on_fail`` can be passed to any requirements +runner:: + + # to Permission helper + Permission(SomeRequirement(), on_fail=flash_failure_message) + + # to decorators + @allow.requires(SomeRequirement(), on_fail=redirect_to_home) + @requires(SomeRequirement(), on_fail=redirect_to_home) diff --git a/docs/helpers.rst b/docs/helpers.rst new file mode 100644 index 0000000..4fcec35 --- /dev/null +++ b/docs/helpers.rst @@ -0,0 +1,113 @@ +.. _helpers: + + +####### +Helpers +####### + +In addition to the :class:`~flask_allows.allows.Allows`, there are several +helper classes and functions available. + + +********** +Permission +********** + +:class:`~flask_allows.permission.Permission` enables checking permissions as a +boolean or controlling access to sections of code with a context manager. To +construct a Permission, provide it with a collection of requirements to enforce +and optionally any combination of: + +- ``on_fail`` callback +- An exception type or instance with ``throws`` +- A specific identity to check against + +.. note:: + + Constructing a ``Permission`` object requires an application context as it + gathers defaults from the configured Allows extension at construction time. + +Once configured, the Permission object can be used as if it were a boolean:: + + p = Permission(SomeRequirement()) + + if p: + print("Passed!") + else: + print("Failed!") + +When using Permission as a boolean, only the requirement checks are run but no +failure handling is run as not entering the conditional block is considered +handling the failure. Not running the failure handling on a failed conditional +check also helps cut down on unexpected side effects. + + +If you'd like the failure handlers to be run, Permission can also be used as a +context manager:: + + p = Permission(SomeRequirement()) + + with p: + print("Passed!") + +When used as a context manager, if the requirements provided are not met then +the registered ``on_fail`` callback is run and the registered exception type +is raised. + +.. note:: + + Permission ignores the result of the callback when used as a context + manager so the exception type is always raised unless the callback raises + an exception instead. + + +******** +requires +******** + +If you're using factory methods to create your Flask application and extensions, +it's often difficult to get ahold of a reference to the allows object. Because +of this, the :func:`~flask_allows.view.requires` helper exists as well. This +is a function that calls the configured allows object when the wrapped function +is invoked:: + + @requires(SomeRequirement()) + def random(): + return 4 + +.. danger:: + + If you're using ``requires`` to guard route handlers, the :func:``route`` + decorator must be applied at the top of the decorator stack (visually first, + logically last):: + + @app.route('/') + @requires(SomeRequirement()) + def index(): + pass + + If the ``requires`` decorator comes after the ``route`` decorator, then the + unguarded function is registered into the application:: + + @requires(SomeRequirement()) + @app.route('/') + def index(): + pass + + This invocation registers the actual ``index`` function into the routing + map and then decorates the index function. + + +The ``requires`` decorator can also be applied to class based views by either +adding it to the ``decorators`` property:: + + class SomeView(View): + decorators = [requires(SomeRequirement())] + +Or by decorating individual methods:: + + class SomeView(MethodView): + + @requires(SomeRequirement()) + def get(self): + ... diff --git a/docs/index.rst b/docs/index.rst index c7cb19f..47ddc3a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,7 +34,7 @@ Content :maxdepth: 2 quickstart - creating_requirements - using_requirements + helpers + failure api diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 1774504..a862ae3 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -67,7 +67,8 @@ Guarding Routes In order to guard route handlers, two decorators are provided: -- The :meth:`~flask_allows.allows.Allows.requires` method on the configured Allows instance +- The :meth:`~flask_allows.allows.Allows.requires` method on the configured + Allows instance - The standalone :meth:`~flask_allows.views.requires` Both accept the same arguments the only difference is where each is diff --git a/docs/using_requirements.rst b/docs/using_requirements.rst deleted file mode 100644 index a9a2928..0000000 --- a/docs/using_requirements.rst +++ /dev/null @@ -1,56 +0,0 @@ -.. _using_requirements: - -################## -Using Requirements -################## - -Outside of calling the requirement yourself, there are two main ways to make -use of Requirements: - -1. Via a decorator, either ``Allows.requires`` or ``requires`` -2. Using the :class:`~flask_allows.permission.Permission` helper - - -To apply requirements with the decorators, apply the decorator to the route -handler, or in the list of decorators for class based views, and pass the -desired requirements as positional arguments:: - - @requires(HasPermission('view_frob'), user_can_access_frob) - def view_frob(frob_id): - pass - - class AView(View): - decorators = [requires(HasPermission('admin'))] - -.. note:: - - Using the standalone :meth:`~flask_allows.views.requires` decorator - requires a :class:`~flask_allows.allows.Allows` instance configured - against the current application. - - -An alternative usage method is via the -:class:`~flask_allows.permission.Permission` helper, for these examples let's -consider the following instance of the Permission class:: - - from flask_allows import Permission - - perm = Permission(HasPermission('view_frob')) - -It can be used as a boolean value, such as in an if check:: - - if perm: - # show the frob - -In this usage, the Permission instance will check if the current identity and -request fulfills all the provided requirements and return True or False based -on this. - -The second usage is in a context manager fashion:: - - with perm: - # stuff - -In this case, if every requirement isn't fulfilled, then the Permission fails -by calling its ``on_fail`` handler and raising the exception it is configured -with.