Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 160 auth policy #162

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a593ebd
Create draft PR for #155
Flix6x Jul 20, 2021
f5723ab
Two db migrations introducing the new tables GenericAssetType and Gen…
Flix6x Jul 20, 2021
6554d6c
Fix tests
Flix6x Jul 27, 2021
fd4770f
Update CLI command for adding a sensor
Flix6x Jul 27, 2021
5482343
Resolve type warnings
Flix6x Jul 27, 2021
dd6c551
Add CLI commands to create generic assets and generic asset types, an…
Flix6x Jul 27, 2021
330d4f7
allow to generate db schema pics (with --uml) including the new dev m…
nhoening Aug 2, 2021
0f8b812
Create draft PR for #158
nhoening Aug 11, 2021
0bce19d
Merge branch 'issue-155-Group_sensors_by_generic_asset' into issue-15…
nhoening Aug 11, 2021
535229e
add account table and according data migration script
nhoening Aug 12, 2021
a4eec8f
fix the preservation of generic_asset.owner_id information between up…
nhoening Aug 13, 2021
3f154c3
mention account name on user page and listings
nhoening Aug 13, 2021
00a1d4e
change name of /account page to /logged-in-user to avoid confusion
nhoening Aug 14, 2021
8a30b14
adapt data.services.users:create_user to deal with the account_id; ma…
nhoening Aug 14, 2021
a3e58db
display account name on logged-in-user page, not account ID
nhoening Aug 14, 2021
d80338e
give automatically generated generoc assets the correct account ID (d…
nhoening Aug 15, 2021
98e0d30
Support adding & deleting accounts in CLI, also add --account-id to t…
nhoening Aug 15, 2021
8d6819d
API: protect change in asset.owner_id from switching accounts
nhoening Aug 16, 2021
d50caab
a few user/asset API docs improvements found on the way, also adding …
nhoening Aug 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/api/introduction.rst
Expand Up @@ -102,7 +102,7 @@ A fresh "<token>" can be generated on the user's profile after logging in:

.. code-block:: html

https://company.flexmeasures.io/account
https://company.flexmeasures.io/logged-in-user

or through a POST request to the following endpoint:

Expand Down
7 changes: 7 additions & 0 deletions documentation/cli/change_log.rst
Expand Up @@ -4,6 +4,13 @@
FlexMeasures CLI Changelog
**********************


since v0.6.0 | April 2, 2021
=====================

* add ``flexmeasures add account``, ``flexmeasures delete account`` and the ``--account-id`` param to ``flexmeasures add user``.


since v0.4.0 | April 2, 2021
=====================

Expand Down
2 changes: 2 additions & 0 deletions documentation/cli/commands.rst
Expand Up @@ -25,6 +25,7 @@ of which some are referred to in this documentation.
================================================= =======================================
``flexmeasures add structure`` Initialize structural data like asset types,
market types and weather sensor types.
``flexmeasures add account`` Create a FlexMeasures tenant account.
``flexmeasures add user`` Create a FlexMeasures user.
``flexmeasures add asset`` Create a new asset.
``flexmeasures add weather-sensor`` Add a weather sensor.
Expand All @@ -39,6 +40,7 @@ of which some are referred to in this documentation.
================================================= =======================================
``flexmeasures delete structure`` Delete all structural (non time-series) data like assets (types),
markets (types) and weather sensors (types) and users.
``flexmeasures delete account`` Delete a tenant account & also their users (with assets and power measurements).
``flexmeasures delete user`` Delete a user & also their assets and power measurements.
``flexmeasures delete measurements`` Delete measurements (with horizon <= 0).
``flexmeasures delete prognoses`` Delete forecasts and schedules (forecasts > 0).
Expand Down
20 changes: 14 additions & 6 deletions documentation/getting-started.rst
Expand Up @@ -96,18 +96,26 @@ This suffices for a quick start.



Add a user
^^^^^^^^^^
Add an account & user
^^^^^^^^^^^^^^^^^^^^^

FlexMeasures is a tenant-based platform ― multiple clients can enjoy its services on one server. Let's create a tenant account first:

.. code-block::

flexmeasures add account --name "Some company"

This command will tell us the ID of this account. Let's assume it was ``2``.

FlexMeasures is a web-based platform, so we need a user account:
FlexMeasures is also a web-based platform, so we need to create a user to authenticate:

.. code-block::

flexmeasures add user --username <your-username> --email <your-email-address> --roles=admin
flexmeasures add user --username <your-username> --email <your-email-address> --account-id 2 --roles=admin


* This will ask you to set a password for the user.
* Giving the first user the ``admin`` role is probably what you want.
* Giving the first user the ``admin`` role is probably what you want. TODO: is `admin` the new super-user?


Add structure
Expand Down Expand Up @@ -215,4 +223,4 @@ More information (e.g. for installing on Windows) on `the Cbc website <https://p
Install and configure Redis
^^^^^^^^^^^^^^^^^^^^^^^

To let FlexMeasures queue forecasting and scheduling jobs, install a `Redis <https://redis.io/>`_ server (or rent one) and configure access to it within FlexMeasures' config file (see above). You can find the necessary settings in :ref:`redis-config`.
To let FlexMeasures queue forecasting and scheduling jobs, install a `Redis <https://redis.io/>`_ server (or rent one) and configure access to it within FlexMeasures' config file (see above). You can find the necessary settings in :ref:`redis-config`.
4 changes: 2 additions & 2 deletions documentation/views/admin.rst
Expand Up @@ -4,7 +4,7 @@
Administration
**************

The administrator can edit assets and user accounts here.
The administrator can edit assets and users here.

Assets
------
Expand Down Expand Up @@ -36,4 +36,4 @@ Viewing one user:

.. image:: https://github.com/SeitaBV/screenshots/raw/main/screenshot_user.png
:align: center
.. :scale: 40%
.. :scale: 40%
2 changes: 1 addition & 1 deletion flexmeasures/api/common/schemas/tests/test_sensors.py
Expand Up @@ -14,7 +14,7 @@
build_entity_address(dict(sensor_id=1), "sensor"),
"sensor",
"fm1",
"my daughter's height",
"height",
),
(
build_entity_address(
Expand Down
17 changes: 15 additions & 2 deletions flexmeasures/api/dev/tests/conftest.py
Expand Up @@ -3,6 +3,7 @@
from flask_security import SQLAlchemySessionUserDatastore
import pytest

from flexmeasures.data.models.generic_assets import GenericAssetType, GenericAsset
from flexmeasures.data.models.time_series import Sensor


Expand All @@ -28,13 +29,25 @@ def setup_api_fresh_test_data(fresh_db, setup_roles_users_fresh_db):
give_prosumer_the_MDC_role(fresh_db)


def add_gas_sensor(the_db, test_supplier):
def add_gas_sensor(db, test_supplier):
incineration_type = GenericAssetType(
name="waste incinerator",
)
db.session.add(incineration_type)
db.session.flush()
incineration_asset = GenericAsset(
name="incineration line",
generic_asset_type=incineration_type,
)
db.session.add(incineration_asset)
db.session.flush()
gas_sensor = Sensor(
name="some gas sensor",
unit="m³/h",
event_resolution=timedelta(minutes=10),
generic_asset=incineration_asset,
)
the_db.session.add(gas_sensor)
db.session.add(gas_sensor)
gas_sensor.owner = test_supplier


Expand Down
3 changes: 2 additions & 1 deletion flexmeasures/api/tests/conftest.py
Expand Up @@ -8,7 +8,7 @@


@pytest.fixture(scope="module", autouse=True)
def setup_api_test_data(db, setup_roles_users):
def setup_api_test_data(db, setup_account, setup_roles_users):
"""
Adding the task-runner
"""
Expand All @@ -31,6 +31,7 @@ def setup_api_test_data(db, setup_roles_users):
username="test user",
email="task_runner@seita.nl",
password=hash_password("testtest"),
account_id=setup_account.id,
)
user_datastore.add_role_to_user(test_task_runner, test_task_runner_role)

Expand Down
8 changes: 7 additions & 1 deletion flexmeasures/api/v1/tests/conftest.py
Expand Up @@ -10,20 +10,22 @@


@pytest.fixture(scope="module", autouse=True)
def setup_api_test_data(db, setup_roles_users, add_market_prices):
def setup_api_test_data(db, setup_account, setup_roles_users, add_market_prices):
"""
Set up data for API v1 tests.
"""
print("Setting up data for API v1 tests on %s" % db.engine)

from flexmeasures.data.models.assets import Asset, AssetType, Power
from flexmeasures.data.models.generic_assets import GenericAssetType
from flexmeasures.data.models.data_sources import DataSource

# Create an anonymous user
test_anonymous_prosumer = create_user(
username="anonymous user with Prosumer role",
email="demo@seita.nl",
password=hash_password("testtest"),
account_name=setup_account.name,
user_roles=[
"Prosumer",
dict(name="anonymous", description="Anonymous test user"),
Expand All @@ -33,6 +35,7 @@ def setup_api_test_data(db, setup_roles_users, add_market_prices):
# Create 1 test asset for the anonymous user
test_asset_type = AssetType(name="test-type")
db.session.add(test_asset_type)
db.session.add(GenericAssetType(name="test-type"))
asset_names = ["CS 0"]
assets: List[Asset] = []
for asset_name in asset_names:
Expand All @@ -54,6 +57,7 @@ def setup_api_test_data(db, setup_roles_users, add_market_prices):
username="test user without roles",
email="test_user@seita.nl",
password=hash_password("testtest"),
account_name=setup_account.name,
)

# Create 5 test assets for the test_prosumer user
Expand Down Expand Up @@ -115,11 +119,13 @@ def setup_fresh_api_test_data(fresh_db, setup_roles_users_fresh_db):
db = fresh_db
setup_roles_users = setup_roles_users_fresh_db
from flexmeasures.data.models.assets import Asset, AssetType
from flexmeasures.data.models.generic_assets import GenericAssetType

# Create 5 test assets for the test_prosumer user
test_prosumer = setup_roles_users["Test Prosumer"]
test_asset_type = AssetType(name="test-type")
db.session.add(test_asset_type)
db.session.add(GenericAssetType(name="test-type"))
asset_names = ["CS 1", "CS 2", "CS 3", "CS 4", "CS 5"]
assets: List[Asset] = []
for asset_name in asset_names:
Expand Down
6 changes: 5 additions & 1 deletion flexmeasures/api/v1_1/tests/conftest.py
Expand Up @@ -12,14 +12,15 @@


@pytest.fixture(scope="module")
def setup_api_test_data(db, setup_roles_users, add_market_prices):
def setup_api_test_data(db, setup_account, setup_roles_users, add_market_prices):
"""
Set up data for API v1.1 tests.
"""
print("Setting up data for API v1.1 tests on %s" % db.engine)

from flexmeasures.data.models.user import User, Role
from flexmeasures.data.models.assets import Asset, AssetType
from flexmeasures.data.models.generic_assets import GenericAssetType

user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)

Expand All @@ -28,6 +29,7 @@ def setup_api_test_data(db, setup_roles_users, add_market_prices):
username="test user with improper registration",
email="test_improper_user@seita.nl",
password=hash_password("testtest"),
account_id=setup_account.id,
)
role = user_datastore.find_role("Prosumer")
user_datastore.add_role_to_user(user, role)
Expand All @@ -37,12 +39,14 @@ def setup_api_test_data(db, setup_roles_users, add_market_prices):
username="test user without roles",
email="test_user@seita.nl",
password=hash_password("testtest"),
account_name=setup_account.name,
)

# Create 3 test assets for the test_prosumer user
test_prosumer = setup_roles_users["Test Prosumer"]
test_asset_type = AssetType(name="test-type")
db.session.add(test_asset_type)
db.session.add(GenericAssetType(name="test-type"))
asset_names = ["CS 1", "CS 2", "CS 3"]
assets: List[Asset] = []
for asset_name in asset_names:
Expand Down
16 changes: 16 additions & 0 deletions flexmeasures/api/v2_0/implementations/assets.py
Expand Up @@ -9,6 +9,7 @@
from marshmallow import fields

from flexmeasures.data.services.resources import get_assets
from flexmeasures.data.models.user import User
from flexmeasures.data.models.assets import Asset as AssetModel
from flexmeasures.data.schemas.assets import AssetSchema
from flexmeasures.data.auth_setup import unauthorized_handler
Expand Down Expand Up @@ -115,6 +116,19 @@ def decorated_endpoint(*args, **kwargs):
return wrapper


def ensure_asset_remains_in_account(db_asset: AssetModel, new_owner_id: int):
"""
Temporary protection of information kept in two places
(Asset.owner_id, GenericAsset.account_id) until we use GenericAssets throughout.
"""
new_owner = User.query.get(new_owner_id)
if new_owner and new_owner.account != db_asset.owner.account:
raise abort(
400,
f"New owner {new_owner_id} not allowed, belongs to different account than current owner.",
)


@load_asset()
@as_json
def fetch_one(asset):
Expand All @@ -129,6 +143,8 @@ def patch(db_asset, asset_data):
"""Update an asset given its identifier"""
ignored_fields = ["id"]
for k, v in [(k, v) for k, v in asset_data.items() if k not in ignored_fields]:
if k == "owner_id":
ensure_asset_remains_in_account(db_asset, v)
setattr(db_asset, k, v)
db.session.add(db_asset)
try:
Expand Down
28 changes: 15 additions & 13 deletions flexmeasures/api/v2_0/routes.py
Expand Up @@ -47,14 +47,14 @@
)
v2_0_service_listing["services"].append(
{
"name": "PATCH /assets/<id>",
"name": "PATCH /asset/<id>",
"access": [],
"description": "Edit an asset.",
},
)
v2_0_service_listing["services"].append(
{
"name": "DELETE /assets/<id>",
"name": "DELETE /asset/<id>",
"access": [],
"description": "Delete an asset and its data.",
},
Expand Down Expand Up @@ -117,11 +117,11 @@ def get_assets():
"id": 1,
"latitude": 10,
"longitude": 100,
"market": 1,
"market_id": 1,
"max_soc_in_mwh": 5,
"min_soc_in_mwh": 0,
"name": "Test battery",
"owner": 2,
"owner_id": 2,
"soc_datetime": "2015-01-01T00:00:00+00:00",
"soc_in_mwh": 2.5,
"soc_udi_event_id": 203,
Expand Down Expand Up @@ -166,8 +166,8 @@ def post_assets():
"name": "Test battery",
"asset_type": "battery",
"unit": "kW",
"owner": 2,
"market": 1,
"owner_id": 2,
"market_id": 1,
"event_resolution": 5,
"capacity_in_mw": 4.2,
"latitude": 40,
Expand Down Expand Up @@ -197,8 +197,8 @@ def post_assets():
"max_soc_in_mwh": 5,
"min_soc_in_mwh": 0,
"name": "Test battery",
"owner": 2,
"market": 1,
"owner_id": 2,
"market_id": 1,
"soc_datetime": null,
"soc_in_mwh": null,
"soc_udi_event_id": null
Expand Down Expand Up @@ -238,11 +238,11 @@ def get_asset(id: int):
"id": 1,
"latitude": 10,
"longitude": 100,
"market": 1,
"market_id": 1,
"max_soc_in_mwh": 5,
"min_soc_in_mwh": 0,
"name": "Test battery",
"owner": 2,
"owner_id": 2,
"soc_datetime": "2015-01-01T00:00:00+00:00",
"soc_in_mwh": 2.5,
"soc_udi_event_id": 203,
Expand Down Expand Up @@ -300,11 +300,11 @@ def patch_asset(id: int):
"id": 1,
"latitude": 11.1,
"longitude": 99.9,
"market": 1,
"market_id": 1,
"max_soc_in_mwh": 5,
"min_soc_in_mwh": 0,
"name": "Test battery",
"owner": 2,
"owner_id": 2,
"soc_datetime": "2015-01-01T00:00:00+00:00",
"soc_in_mwh": 2.5,
"soc_udi_event_id": 203,
Expand Down Expand Up @@ -403,6 +403,7 @@ def get_user(id: int):
.. sourcecode:: json

{
'account_id': 1,
'active': True,
'email': 'test_prosumer@seita.nl',
'flexmeasures_roles': [1, 3],
Expand Down Expand Up @@ -435,7 +436,7 @@ def patch_user(id: int):
Only the user themselves or admins are allowed to update its data,
while a non-admin can only edit a few of their own fields.

Several fields are not allowed to be updated, e.g. id. They are ignored.
Several fields are not allowed to be updated, e.g. id and account_id. They are ignored.

**Example request**

Expand All @@ -452,6 +453,7 @@ def patch_user(id: int):
.. sourcecode:: json

{
'account_id': 1,
'active': True,
'email': 'test_prosumer@seita.nl',
'flexmeasures_roles': [1, 3],
Expand Down