Manipulating Requirements after the fact

Since requirements applied to route handlers are static, they can be quite difficult to manipulate after the fact. Fancy foot work with requirement factories can ease this some but at the cost of complexity, manual management and potentially tricky application or request scoped context locals.

To address this, flask-allows provides a mechanism for overriding and adding additional requirements itself.

Note

In order to use these features with Class Based Requirements, you must define both an __eq__ and __hash__ method on the requirement:

class Has(Requirement):
    def __init__(self, permission):
        self.permission = permission

    def fulfill(self, user):
        return self.permission in user.permissions

    def __eq__(self, other):
        return isinstance(other, Has) and self.permission == other.permission

    def __hash__(self):
        return hash(self.permission)

Since this quite a bit of boilerplate, consider using attrs in conjunction with this library as well:

import attr


@attr.s(frozen=True)
class Has(Requirement):
    permission = attr.ib()

    def fulfill(self, user):
        return

The downside here is that Has also becomes orderable, but see python-attrs/attrs #170 for more details.

Disabling Requirements

Disabling requirements can be useful to temporarily allows a specific user access to certain areas of your application. flask-allows exposes an overrides attribute on the extension object, as well as providing a current_overrides context local and an Override class, each of these play a separate role in the process:

  • allows.overrides is the OverrideManager instance associated with the extension object. It is strongly recommended to use this instance rather than instantiating your own.
  • current_overrides is a context local that points towards the current override context.
  • Override is the representation of the override context.

Note

current_overrides is a context local managed separately from the application and requests contexts. However, the Allows extension object registers before and after request handlers to push and cleanup override contexts.

flask-allows automatically starts an override context at the beginning of a request so we can immediately being overriding requirements by calling add():

from flask_allows import current_overrides
from .app.requirements import is_admin

current_overrides.add(is_admin)

We can also remove a requirement from the override context with remove():

current_overrides.remove(is_admin)

Both add and remove accept multiple requirements but must always be passed at least one requirement.

Note

Adding and removing from current_overrides affects the current context directly. If this is an object you’re holding a reference to, you will see the changes reflected in it.

It is possible to temporarily replace the current context with a new one with OverrideManager’s override() method which acts as a context manager:

with allows.overrides.override(Override(is_admin)):
    ...

When the block is entered, a new override context is pushed and when the block exits, it is popped. This context manager also yields the new context into the block for convenience sake:

with allows.overrides.override(Override(is_admin)) as overrides:
    ...

If the new context should augment rather than entirely replace the current context, you may supply the use_parent argument to override:

with allows.overrides.override(Override(is_admin), use_parent=True):
    ...

Behind the scenes, this creates a new Override instance that combines the disabled requirements from the current context and the child context rather than changing either’s state directly. This makes transitioning back to the original context easier.

If we need to check if the current override context overrides a requirement, that is possible with either the is_overridden method or the in operator:

current_overrides.is_overridden(is_admin)
is_admin in current_overrides

Manually managing override contexts

We can also manually manage overrides on a global scale by using the manager’s push() and pop() methods. This can be useful when working outside the request-response cycle, such as in a CLI context or out-of-band task runner such as celery.

Danger

pop() checks that the popped context belongs to the manager instance that popped the context. If a separate manager instance pushed the last context or if a context was not active when pop was called, a RuntimeError is raised to signal this error.

To begin a manual override context we must first call push method with an Override instance:

allows.overrides.push(Override())

This replaces the current context rather than augments it and current_overrides points at this instance. If newly pushed context should augment the existing context rather than replacing it entirely, you may supply the use_parent argument – this behaves the same as when provided with the manager’s override method.

When we are done with this context, we must call the pop method to end the context and replace it with its parent:

allows.overrides.pop()

Adding More Requirements

In a similar vein as the OverrideManager, you may also add more requirements to the context as well. To achieve this, flask-allows exposes an additional attribute on the extension object, as well as a current_additions and an Additional class, each plays a similar role to their override counterparts:

  • allows.additional is the AdditionalManager instance associated with the extension object. It’s strongly recommended to use this instance rather than instantiating your own.
  • current_additions is a context local that points towards the current additional context.
  • Additional is the representation of the additional context.

Note

current_additions is a context local managed separately from the application and requests contexts. However, the Allows extension object registers before and after request handlers to push and cleanup additional contexts.

flask-allows manages additional contexts in the same fashion as an override context, automatically starting and ending the context in tune with the request cycle:

from flask_allows import current_additions
from .myapp.requirements import is_admin

current_additions.add(is_admin)

And removing the additional requirement:

current_additions.remove(is_admin)

add and remove can accept multiple arguments but must always be passed at least one requirement.

It is also possible to temporarily replace the current additional context with a new one by using the AdditionalManager’s additional() method:

with allows.additional.additional(Additional(is_admin)):
    ...

Just like with OverrideManager this method will inject the new context into the block and can accept a use_parent argument to combine the new context and the current context into one:

with allows.additional.additional(Additional(is_admin), use_parent) as added:
    assert added.is_added(is_admin)

Additional objects can be checked for membership using either the is_added() method or with in:

current_additions.add(is_admin)
current_additions.is_added(is_admin)
is_admin in current_additions

And Additional instances may be length checked and iterated as well:

current_additions.add(is_admin)
assert len(current_additions) == 1
assert list(current_additions) == [is_admin]

Manually Managing Additional Contexts

Additional contexts can also be managed manually at the global level with the push() and pop() methods. This can be useful when working outside the request cycle such as in an out of band task worker such as celery.

Danger

pop() checks that the popped context belongs to the manager instance that popped the context. If a separate manager instance pushed the last context or if a context was not active when pop was called, a RuntimeError is raised to signal this error.

To being managing the context, we must first call the manager’s push method with an Additional instance:

allows.overrides.push(Additional(is_admin))

This replaces the current context rather than augmenting it and current_additions will being pointing at this context. If augmenting is preferred, the use_parent argument can be passed, this behaves the same as when provided to the additional method.

When we are finished with this context, we must called the pop method to remove the context and restore its parent:

allows.additional.pop()