Skip to content

Commit

Permalink
update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
nhoening committed Nov 17, 2021
1 parent 0ef64a1 commit 2aaeb70
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 33 deletions.
19 changes: 12 additions & 7 deletions documentation/concepts/security_auth.rst
Expand Up @@ -15,7 +15,7 @@ There are two types of data on FlexMeasures servers - files (e.g. source code, i
* Finally, The application communicates all data with HTTPS, the Hypertext Transfer Protocol encrypted by Transport Layer Security. This is used even if the application is accessed via ``http://``.


.. _auth:
.. _authentication:

Authentication
----------------
Expand All @@ -30,19 +30,24 @@ This involves a username/password combination ("credentials") or an access token
.. note:: Authentication (and authorization, see below) affects the FlexMeasures API and UI. The CLI (command line interface) can only be used if the user is already on the server and can execute ``flexmeasures`` commands, thus we can safely assume they are admins.


.. _authorization:

Authorization
--------------

*Authorization* is the system by which the FlexMeasures platform decides whether an authenticated user can access a feature. For instance, many features are reserved for administrators, others for users belonging to certain accounts. An example for the latter is that a user might need to belong to an account with the "Prosumer" account role (usually the owner of assets).
*Authorization* is the system by which the FlexMeasures platform decides whether an authenticated user can access data. Data about users and assets. Or metering data, forecasts and schedules.

For instance, a user is authorized to update his or her personal data, like the surname. Other users should not be authorized to do that. We can also authorize users to do something because they belong to a certain account. An example for this is to read the meter data of the account's assets. Any regular user should *only* be able to read data that their account should be able to see.

.. note:: Each user belongs to exactly one account.

.. todo:: Data which belongs to a specific account should only be viewable by users within that account (and platform admins). We want to anchor this crucial security measure on a deep level, `see this ticket <https://github.com/SeitaBV/flexmeasures/issues/201>`_.
In a nutshell, the way FlexMeasures implements authorization works as follows: The data models codify under which conditions a user can have certain permissions to work with their data. Permissions allow distinct ways of access like reading, writing or deleting. The API endpoints are where we know what needs to happen to what data, so there we make sure that the user has the necessary permissions.

All other authorization is achieved via *roles*.
We already discussed certain conditions under which a user has access to data ― being a certain user or belonging to a specific account. Furthermore, authorization conditions can also be implemented via *roles*:

* Account roles are most commonly used for deciding who can access a resource (usually guarded by an API endpoint). We support several roles which are mentioned in the USEF framework but more roles are possible (e.g. defined by customer services, see below).
* User roles give a user personal authorizations. For instance, we have a few `admin`\ s who can perform all actions, and `admin-reader`\ s who can read everything. Other roles have only an effect within the user's account.
* ``Account roles`` are often used for authorization. We support several roles which are mentioned in the USEF framework but more roles are possible (e.g. defined by custom-made services, see below). For example, a user might be authorized to write sensor data if they belong to an account with the "MDC" account role ("MDC" being short for meter data company).
* ``User roles`` give a user personal authorizations. For instance, we have a few `admin`\ s who can perform all actions, and `admin-reader`\ s who can read everything. Other roles have only an effect within the user's account, e.g. there could be an "HR" role which allows to edit user data like surnames within the account.
* Roles cannot be edited via the UI at the moment. They are decided when a user or account is created in the CLI (for adding roles later, we use the database for now). Editing roles in UI and CLI is future work.

.. note:: Custom energy flexibility services developed on top of FlexMeasures can use account roles to achieve their custom authorization. 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. More on this in :ref:`auth-dev`.

.. note:: Custom energy flexibility services developed on top of FlexMeasures also need to implement authorization. More on this in :ref:`auth-dev`. Here is an example for a custom authorization concept: services can use account roles to achieve their custom authorization. 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.
53 changes: 47 additions & 6 deletions documentation/dev/auth.rst
Expand Up @@ -3,9 +3,47 @@
Custom authorization
======================

Our :ref:`auth` section describes general authentication and authorization handling in FlexMeasures. However, custom energy flexibility services developed on top of FlexMeasures probably also need their custom authorization.
Our :ref:`authorization` section describes general authorization handling in FlexMeasures.

One means for this 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:
If you are creating your own API endpoints for a custom energy flexibility services (on top of FlexMeasures), you should also get your authorization right.

This comment has been minimized.

Copy link
@Flix6x

Flix6x Nov 17, 2021

Contributor

a service (singular)

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 authorization decorators are straightforward. However, they are a bit crude as they do not qualify on the permission (e.g. read versus write). A consequence of this is that the ``admin-reader`` role cannot be checked in role-based decorators.

This comment has been minimized.

Copy link
@Flix6x

Flix6x Nov 17, 2021

Contributor
  • I wouldn't presume that these authorization decorators are straightforward to the reader.
  • You mention a consequence, but its practical implications aren't really clear to me.

Finally, all decorators available through `Flask-Security-Too <https://flask-security-too.readthedocs.io/en/stable/patterns.html#authentication-and-authorization>`_ can be used, e.g. ``@auth_required`` (that's technically only checking authentication) or ``@permissions_required``.


Permission-based authorization
--------------------------------

Via permissions, it's possible to define authorization access to data, distinguishing between create, read, update and delete access. It's a finer model than simply allowing per role.

The data models codify under which conditions a user can have certain permissions to work with their data.
You, as the endpoint author, need to make sure this is checked. Here is an example (taken from the decorator docstring):

.. code-block:: python
@app.route("/resource/<resource_id>", methods=["GET"])
@use_kwargs(
{"the_resource": ResourceIdField(data_key="resource_id")},
location="path",
)
@permission_required_for_context("read", arg_name="the_resource")
@as_json
def view(resource_id: int, resource: Resource):

This comment has been minimized.

Copy link
@Flix6x

Flix6x Nov 17, 2021

Contributor

Is the original resource_id also passed next to the deserialized resource? I expected to see only the latter here.

This comment has been minimized.

Copy link
@nhoening

nhoening Nov 17, 2021

Author Contributor

My custom factory approach replaced the id argument in the list of args. Marshmallow only adds new things to the arguments. I spent considerable time on this, as I share your view, but that is how it is.

return dict(name=resource.name)
As you see, there is some sorcery with ``@use_kwargs`` going on before we check the permissions. `That decorator <https://webargs.readthedocs.io>`_ is relaying to a `Marshmallow <https://marshmallow.readthedocs.io/>`_ field definition. Here, ``ResourceIdField`` is a definition which de-serializes an ID (passed in as a request parameter) into a ``Resource`` instance. This instance can then be asked if the current user may read it. That last part is what ``@permission_required_for_context`` is doing. You can find these Marshmallow fields in ``flexmeasures.api.common.schemas``.


Account roles
---------------

One means for this 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.

This comment has been minimized.

Copy link
@Flix6x

Flix6x Nov 17, 2021

Contributor

One means for this

Missing context; what does "this" refer to?


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

.. code-block:: python
Expand All @@ -14,9 +52,13 @@ One means for this is to define custom account roles. E.g. if several services r
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.
.. 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.


There are also decorators to check user roles:
User roles
---------------

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

.. code-block:: python
Expand All @@ -25,5 +67,4 @@ There are also decorators to check user roles:
def bananas_view:
pass
.. note:: You can also use the ``roles_accepted`` decorator.

.. note:: You can also use the ``@roles_accepted`` decorator.
26 changes: 6 additions & 20 deletions flexmeasures/auth/decorators.py
Expand Up @@ -18,22 +18,6 @@
)


"""
TODO - For developer docs:
FlexMeasures supports the the following role-based decorators:
- roles_accepted
- roles_required
- account_roles_accepted
- account_roles_required
However, these do not qualify on the permission.
A finer auth model is available to distinguish between create, read, write and delete access.
One direct drawback is that the admin-reader role cannot be checked in role-based decorators.
Therefore, we recommend to use the permission_required_for_context decorator.
"""
_security = LocalProxy(lambda: current_app.extensions["security"])


Expand Down Expand Up @@ -155,6 +139,7 @@ def decorated_view(*args, **kwargs):
)
if current_user.is_anonymous:
return _security._unauthn_handler()
# load & check context
if arg_pos is not None and arg_name is not None:
context: AuthModelMixin = args[arg_pos][arg_name]
elif arg_pos is not None:
Expand All @@ -163,20 +148,21 @@ def decorated_view(*args, **kwargs):
context = kwargs[arg_name]
else:
context = args[0]
if not isinstance(context, AuthModelMixin):
if context is None:
current_app.logger.error(
f"Context {context} needs {permission}-permission, but is no AuthModelMixin."
f"Context needs {permission}-permission, but no context was passed."
)
return _security._unauthz_handler(
permission_required_for_context, (permission,)
)
if context is None:
if not isinstance(context, AuthModelMixin):
current_app.logger.error(
f"Context needs {permission}-permission, but no context was passed."
f"Context {context} needs {permission}-permission, but is no AuthModelMixin."
)
return _security._unauthz_handler(
permission_required_for_context, (permission,)
)
# now check access, either with admin rights or principal(s)
acl = context.__acl__()
if not user_has_admin_access(
current_user, permission
Expand Down
1 change: 1 addition & 0 deletions requirements/app.txt
Expand Up @@ -178,6 +178,7 @@ markupsafe==2.0.1
# wtforms
marshmallow==3.14.0
# via
# -r requirements/app.in
# flask-marshmallow
# marshmallow-polyfield
# marshmallow-sqlalchemy
Expand Down

0 comments on commit 2aaeb70

Please sign in to comment.