Skip to content
This repository has been archived by the owner on Jul 21, 2022. It is now read-only.

Commit

Permalink
Rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
justanr committed Apr 15, 2018
1 parent fec50c2 commit 0b946aa
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 199 deletions.
140 changes: 0 additions & 140 deletions docs/creating_requirements.rst

This file was deleted.

98 changes: 98 additions & 0 deletions docs/failure.rst
Original file line number Diff line number Diff line change
@@ -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)
113 changes: 113 additions & 0 deletions docs/helpers.rst
Original file line number Diff line number Diff line change
@@ -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):
...
4 changes: 2 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Content
:maxdepth: 2

quickstart
creating_requirements
using_requirements
helpers
failure
api

3 changes: 2 additions & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 0b946aa

Please sign in to comment.