Requirements¶
Requirements are how routes and code paths are guarded with Flask-Allows, they are also entirely defined by the user. Requirements come in two flavors:
- functions
- class based
Function Requirements¶
Function requirements need to accept the current identity and return a boolean representing if the requirement has been fulfilled or not:
def user_is_admin(user):
return user.level == 'admin'
Note
Optionally, the function may accept the request argument, though this behavior is deprecated as version 0.5.0 and will be removed in 1.0.0:
def user_is_admin(user, request):
return user.level == 'admin'
This function can be provided to any of the requirement runners, if you wanted to guard a route with it:
@app.route('/admin')
@requires(user_is_admin)
def admin():
return render_template('admin.html')
Or guard a particular code path with Permission
:
p = Permission(user_is_admin)
if p:
print("Welcome!")
Class Based Requirements¶
Class based requirements are good if you have something to represent that is
too complicated for a function. While it is possible to implement class based
requirements by adding a __call__()
, there is the
Requirement
that provides the fulfill
hook to implement. It also provides future proofing if new hooks are also
implemented. For example:
class Has(Requirement):
def __init__(self, permission):
self.permission = permission
def fulfill(self, user):
return self.permission in user.permissions
Note
Optionally, the fulfill method may accept the request argument, though this behavior is deprecated as version 0.5.0 and will be removed in 1.0.0:
class Has(Requirement):
def __init__(self, permission):
self.permission = permission
def fulfill(self, user, request):
return self.permission in user.permissions
To apply this to a route:
@app.allows('/admin')
@requires(Has('admin'))
def admin():
return render_template('admin.html')
Danger
If you use class based requirements, you are responsible for instantiating them and providing any necessary arguments to them. A common mistake is providing just the class itself to the requirement runner:
@app.allows('/admin')
@requires(Has)
def admin():
return render_template('admin.html')
This will result in an exception being raised at verification because the identity and request objects are passed into a constructor that only expected one argument. If the constructor expected two arguments, there is a chance that the requirement would incorrectly pass as object default to True when expressed as booleans.
While using Requirement
isn’t strictly necessary, it is provided for people
that prefer an object oriented approach instead.
Combining Requirements¶
In addition to the Requirement
base class,
Flask-Allows also provides a way to combine requirements.
All requirement runners provided by Flask-Allows accept multiple requirements and all must pass for the verification to pass:
@app.route('/admin')
@requires(user_is_logged_in, user_is_admin)
def admin():
return render_template('admin.html')
If either requirement returns False, then the user will not be allowed to access
that route. However, if you have a more complicated requirement, such as a
user must be logged in AND a user must be an admin OR a user must have the
'view_admin_panel'
permission, these can be difficult to express in an
all-or-nothing evaluation strategy.
To handle these situations, Flask-Allows exposes several helper requirements:
from flask_allows import And, Or
@app.route('/admin')
@requires(And(user_is_logged_in, Or(user_is_admin, Has('view_admin_panel'))))
def admin():
return render_template('admin.html')
Strictly speaking, the outer And
isn’t necessary as the requirements will
already be combined in an and
fashion but is presented for example sake.
The And
help is most useful when nested inside of an Or
such as:
Or(user_is_admin, And(Has('view_admin_panel'), user_is_moderator))
Flask-Allows also exposes a helper to invert the result of a requirement:
@app.route('/login')
@requires(Not(user_is_logged_in))
def login():
return render_template('login.html')
Finally, Flask-Allows also exposes a generalized version of these helpers called
ConditionalRequirement
(also importable as
C
to avoid typing out the name every time).
By using ConditionalRequirement
you can build your own requirements combinator.
In addition to the requirements themselves, ConditionalRequirement
will also
accept:
op
a binary operator to reduce results withnegated
if the opposite of the result should be returned (e.g. False turns into True)until
a boolean value to short circuit on and end evaluation
For example, if you needed your requirements combined with xor, that is possible:
from operator import xor
C(perm_1, perm_2, op=xor)
Finally, ConditionalRequirement
also provides the magic methods for:
&
short cut to applyingAnd
between two instances ofConditionalRequirement
|
short cut to applyingOr
between two instances ofConditionalRequirement
~
(invert) short to negating a single instance ofConditionalRequirement
Using these operators, our earlier combined and negated requirements would look like:
C(user_is_logged_in) & (C(user_is_admin) | C(Has('view_admin_panel')))
~C(user_is_logged_in)
However, using the named helper methods are often clearer and more efficient.
Transition to User Only Requirements¶
As of version 0.5, passing the request object directly into a requirement is deprecated and will be removed in version 1.0. Considering the following requirement:
from flask_allows import Requirement
class AllowedToViewPost(Requirement):
def fulfill(self, user, request):
post_id = request.view_args.get('post_id')
if post_id is None:
abort(404)
if post.hidden:
return 'view_hidden_post' in user.permissions
return True
In order to make the transition to a user only requirement, the only change to make is:
from flask import request
from flask_allows import Requirement
class AllowedToViewPost(Requirement):
def fulfill(self, user):
post_id = request.view_args.get('post_id')
if post_id is None:
abort(404)
if post.hidden:
return 'view_hidden_post' in user.permissions
return True
To be clear, request
is now being imported directly from the flask
package. This is the same request object that Allows would pass into the
requirement itself. And the other change is removing the request parameter
from the fulfill
definition.
Behind the scenes, Allows handles both definitions and will dispatch between them as needed.
Danger
If you have a requirement defined with an optional request, such as:
def allowed_to_view_post(user, request=None):
...
Allows will incorrectly determine that you have provided a user only requirement.
If your requirement does not need the request object, the
only change to make is to remove the parameter. If your requirement does
need the request object you may either remove the default value and Allows
will determine that you have provided a user-request requirement, or you
may remove the parameter altogether and import request
directly from
Flask.
Additionally, there is wants_request()
which marks the requirement as user only but passes the current request
behind the scenes. This decorator is intended only to assist during a
transitionary phase and will be remove in flask-allows 1.0