Custom authorization

Our Authorization section describes general authorization handling in FlexMeasures.

If you are creating your own API endpoints for a custom energy flexibility service (on top of FlexMeasures), you should also get your authorization right. It’s recommended to get familiar with the decorators we provide. Here are some pointers, but feel free to read more in the flexmeasures.auth package.

In short, we recommend to use the @permission_required_for_context decorator (more explanation below).

FlexMeasures also supports role-based decorators, e.g. @account_roles_required. These decorators do not check whether the user may perform a named action on a particular resource. [1]

Finally, all decorators available through Flask-Security-Too can be used, e.g. @auth_required (that’s technically only checking authentication) or @permissions_required.

Permission-based authorization

Named permissions describe actions such as read, post-data and trigger-schedules. The names are declared in flexmeasures.auth.policy. The Role.permissions property maps built-in user roles to those names in code; there is no permissions column on Role to populate. A model’s __acl__ maps the same names to principals allowed to perform them on that resource.

An endpoint must check both requirements with @permission_required_for_context or check_access: the user needs an eligible role grant and must match an ACL principal for the resource. For example, trigger-schedules in an asset’s ACL and in an endpoint check refers to the same permission as trigger-schedules in a role’s grants. There is no separate capability identifier or cap: principal. Here is an example (taken from the decorator docstring):

@app.route("/resource/<resource_id>", methods=["GET"])
@use_kwargs(
    {"the_resource": ResourceIdField(data_key="resource_id")},
    location="path",
)
@permission_required_for_context("read", ctx_arg_name="the_resource")
@as_json
def view(resource_id: int, resource: Resource):
    return dict(name=resource.name)

@use_kwargs uses a Marshmallow field to deserialize the ID into a Resource instance. @permission_required_for_context then checks whether the current user may read that instance. You can find these fields in flexmeasures.api.common.schemas.

Home roles (account-member, account-admin, account-reader and account-data-integrator) provide grants only when the matching ACL principal refers to the user’s own account. The consultant role provides grants only through a matching consultancy principal on a client resource. The site-wide admin and admin-reader roles are exceptions. Grant and ACL scope must match in the same ACL alternative; having account-member in a home account does not supply permissions while acting as a consultant for a client account. Unknown user roles grant no built-in named permissions. Custom services can still define their own authorization behavior for their endpoints.

Existing users receive the account-member role in a data migration so their previous implicit home-account access persists; that same migration creates the account-reader and account-data-integrator role rows. Newly created users receive account-member by default unless explicit roles are supplied. Roles only add grants, so remove account-member when converting a user to account-reader or account-data-integrator.

Account roles

Another way to implement custom authorization is to define custom account roles. E.g. if several services run on one FlexMeasures server, each service could define a “MyService-subscriber” account role.

To make sure that only users of such accounts can use the endpoints:

@flexmeasures_ui.route("/bananas")
@account_roles_required("MyService-subscriber")
def bananas_view:
    pass

Note

This endpoint decorator lists required roles, so the authenticated user’s account needs to have each role. You can also use the @account_roles_accepted decorator. Then the user’s account only needs to have at least one of the roles.

User roles

There are also decorators to check user roles. Here is an example:

@flexmeasures_ui.route("/bananas")
@roles_required("account-admin")
def bananas_view:
    pass

Note

You can also use the @roles_accepted decorator.

Footnotes