Skip to content

Commit

Permalink
validate that user can set the account on an asset (#488)
Browse files Browse the repository at this point in the history
* validate that user can set the account on an asset; improve UI for adding assets by non-admins

Signed-off-by: Nicolas Höning <nicolas@seita.nl>

* add test

Signed-off-by: Nicolas Höning <nicolas@seita.nl>

* changelog entry

Signed-off-by: Nicolas Höning <nicolas@seita.nl>

* dedidcated check for showing the delete button, apply creation check to all template rendering calls

Signed-off-by: Nicolas Höning <nicolas@seita.nl>

* fix count of assets in account for logged-in user view; also the link to those assets on the page

Signed-off-by: Nicolas Höning <nicolas@seita.nl>

Signed-off-by: Nicolas Höning <nicolas@seita.nl>
  • Loading branch information
nhoening committed Aug 26, 2022
1 parent 643e4d5 commit c8903b9
Show file tree
Hide file tree
Showing 9 changed files with 82 additions and 14 deletions.
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -24,6 +24,7 @@ Bugfixes
* The docker-based tutorial now works with UI on all platforms (port 5000 did not expose on MacOS) [see `PR #465 <http://www.github.com/FlexMeasures/flexmeasures/pull/465>`_]
* Fix interpretation of scheduling results in toy tutorial [see `PR #466 <http://www.github.com/FlexMeasures/flexmeasures/pull/466>`_ and `PR #475 <http://www.github.com/FlexMeasures/flexmeasures/pull/475>`_]
* Avoid formatting datetime.timedelta durations as nominal ISO durations [see `PR #459 <http://www.github.com/FlexMeasures/flexmeasures/pull/459>`_]
* Account admins cannot add assets to other accounts anymore; and they are shown a button for asset creation in UI [see `PR #488 <http://www.github.com/FlexMeasures/flexmeasures/pull/488>`_]

Infrastructure / Support
----------------------
Expand Down
21 changes: 21 additions & 0 deletions flexmeasures/api/v3_0/tests/test_assets_api.py
Expand Up @@ -212,6 +212,27 @@ def test_post_an_asset_with_existing_name(client, setup_api_test_data):
)


def test_post_an_asset_with_other_account(client, setup_api_test_data):
"""Catch auth error, when account-admin posts an asset for another account"""
with UserContext("test_prosumer_user_2@seita.nl") as account_admin_user:
auth_token = account_admin_user.get_auth_token()
with AccountContext("Test Supplier Account") as supplier:
supplier_id = supplier.id
post_data = get_asset_post_data()
post_data["account_id"] = supplier_id
asset_creation_response = client.post(
url_for("AssetAPI:post"),
json=post_data,
headers={"content-type": "application/json", "Authorization": auth_token},
)
print(f"Creation Response: {asset_creation_response.json}")
assert asset_creation_response.status_code == 422
assert (
"not allowed to create assets for this account"
in asset_creation_response.json["message"]["json"]["account_id"][0]
)


def test_post_an_asset_with_nonexisting_field(client, setup_api_test_data):
"""Posting a field that is unexpected leads to a 422"""
with UserContext("test_admin_user@seita.nl") as prosumer:
Expand Down
4 changes: 2 additions & 2 deletions flexmeasures/cli/data_show.py
Expand Up @@ -109,7 +109,7 @@ def show_account(account):
user.username,
user.email,
naturaltime(user.last_login_at),
"".join([role.name for role in user.roles]),
",".join([role.name for role in user.roles]),
)
for user in users
]
Expand Down Expand Up @@ -187,7 +187,7 @@ def show_generic_asset(asset):
sensor.unit,
naturaldelta(sensor.event_resolution),
sensor.timezone,
"".join([f"{k}:{v}\n" for k, v in sensor.attributes.items()]),
",".join([f"{k}:{v}\n" for k, v in sensor.attributes.items()]),
)
for sensor in sensors
]
Expand Down
9 changes: 9 additions & 0 deletions flexmeasures/data/schemas/generic_assets.py
Expand Up @@ -2,6 +2,7 @@
import json

from marshmallow import validates, validates_schema, ValidationError, fields
from flask_security import current_user

from flexmeasures.data import ma
from flexmeasures.data.models.user import Account
Expand All @@ -11,6 +12,7 @@
MarshmallowClickMixin,
with_appcontext_if_needed,
)
from flexmeasures.auth.policy import user_has_admin_access


class JSON(fields.Field):
Expand Down Expand Up @@ -66,6 +68,13 @@ def validate_account(self, account_id: int):
account = Account.query.get(account_id)
if not account:
raise ValidationError(f"Account with Id {account_id} doesn't exist.")
if (
not user_has_admin_access(current_user, "update")
and account_id != current_user.account_id
):
raise ValidationError(
"User is not allowed to create assets for this account."
)

@validates("latitude")
def validate_latitude(self, latitude: Optional[float]):
Expand Down
33 changes: 30 additions & 3 deletions flexmeasures/ui/crud/assets.py
Expand Up @@ -12,6 +12,7 @@

from flexmeasures.data import db
from flexmeasures.auth.error_handling import unauthorized_handler
from flexmeasures.auth.policy import check_access
from flexmeasures.data.models.generic_assets import (
GenericAssetType,
GenericAsset,
Expand Down Expand Up @@ -48,7 +49,7 @@ class AssetForm(FlaskForm):
places=4,
render_kw={"placeholder": "--Click the map or enter a longitude--"},
)
attributes = StringField("Other attributes (JSON)")
attributes = StringField("Other attributes (JSON)", default="{}")

def validate_on_submit(self):
if (
Expand Down Expand Up @@ -148,6 +149,22 @@ def expunge_asset():
return asset_data


def user_can_create_assets() -> bool:
try:
check_access(current_user.account, "create-children")
except Exception:
return False
return True


def user_can_delete(asset) -> bool:
try:
check_access(asset, "delete")
except Exception:
return False
return True


class AssetCrudUI(FlaskView):
"""
These views help us offer a Jinja2-based UI.
Expand Down Expand Up @@ -186,7 +203,10 @@ def get_asset_by_account(account_id) -> List[GenericAsset]:
assets = get_asset_by_account(current_user.account_id)

return render_flexmeasures_template(
"crud/assets.html", assets=assets, message=msg
"crud/assets.html",
assets=assets,
message=msg,
user_can_create_assets=user_can_create_assets(),
)

@login_required
Expand All @@ -211,14 +231,15 @@ def owned_by(self, account_id: str):
account=Account.query.get(account_id),
assets=assets,
msg=msg,
user_can_create_assets=user_can_create_assets(),
)

@login_required
def get(self, id: str):
"""GET from /assets/<id> where id can be 'new' (and thus the form for asset creation is shown)"""

if id == "new":
if not current_user.has_role("admin"):
if not user_can_create_assets():
return unauthorized_handler(None, [])

asset_form = with_options(NewAssetForm())
Expand Down Expand Up @@ -247,6 +268,8 @@ def get(self, id: str):
latest_measurement_time_str=latest_measurement_time_str,
asset_plot_html=asset_plot_html,
mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""),
user_can_create_assets=user_can_create_assets(),
user_can_delete_asset=user_can_delete(asset),
)

@login_required
Expand Down Expand Up @@ -332,6 +355,8 @@ def post(self, id: str):
latest_measurement_time_str=latest_measurement_time_str,
asset_plot_html=asset_plot_html,
mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""),
user_can_create_assets=user_can_create_assets(),
user_can_delete_asset=user_can_delete(asset),
)
patch_asset_response = InternalApi().patch(
url_for("AssetAPI:patch", id=id),
Expand Down Expand Up @@ -363,6 +388,8 @@ def post(self, id: str):
latest_measurement_time_str=latest_measurement_time_str,
asset_plot_html=asset_plot_html,
mapboxAccessToken=current_app.config.get("MAPBOX_ACCESS_TOKEN", ""),
user_can_create_assets=user_can_create_assets(),
user_can_delete_asset=user_can_delete(asset),
)

@login_required
Expand Down
2 changes: 1 addition & 1 deletion flexmeasures/ui/templates/admin/logged_in_user.html
Expand Up @@ -49,7 +49,7 @@ <h2>User overview</h2>
Assets in account
</td>
<td>
<a href="/assets">{{ num_assets }}</a>
<a href="/assets/owned_by/{{ logged_in_user.account.id }}">{{ num_assets }}</a>
</td>
</tr>
<tr>
Expand Down
4 changes: 3 additions & 1 deletion flexmeasures/ui/templates/crud/asset.html
Expand Up @@ -11,12 +11,14 @@
<div class="row">
<div class="col-sm-2 on-top-md">
<div class="header-action-button">
{% if user_is_admin %}
{% if user_can_create_assets %}
<div class="">
<form action="/assets/new" method="get">
<button class="btn btn-sm btn-responsive btn-success create-button" type="submit">Create new asset</button>
</form>
</div>
{% endif %}
{% if user_can_delete_asset %}
<div class="">
<form action="/assets/delete_with_data/{{ asset.id }}/" method="get">
<button id="delete-asset-button" class="btn btn-sm btn-responsive btn-danger delete-button" type="submit">Delete this asset</button>
Expand Down
9 changes: 7 additions & 2 deletions flexmeasures/ui/templates/crud/assets.html
Expand Up @@ -10,7 +10,12 @@
<div class="row">
<div class="col-sm-12">

<h3>Asset overview</h3>
<h3>Asset overview
{% if account %}
for account {{ account.name }}
{% endif %}
</h3>

<table class="table table-striped table-responsive paginate">
<thead>
<tr>
Expand All @@ -20,7 +25,7 @@ <h3>Asset overview</h3>
<th class="text-right">Account</th>
<th class="text-right no-sort">Sensors</th>
<th class="text-right no-sort">
{% if user_is_admin %}
{% if user_can_create_assets %}
<form action="/assets/new" method="get">
<button class="btn btn-sm btn-responsive btn-success create-button" type="submit">Create new
asset</button>
Expand Down
13 changes: 8 additions & 5 deletions flexmeasures/ui/views/logged_in_user.py
Expand Up @@ -2,20 +2,23 @@
from flask_security import login_required

from flexmeasures.ui.views import flexmeasures_ui
from flexmeasures.data.services.resources import get_assets
from flexmeasures.data.models.generic_assets import GenericAsset
from flexmeasures.ui.utils.view_utils import render_flexmeasures_template


@flexmeasures_ui.route("/logged-in-user", methods=["GET"])
@login_required
def logged_in_user_view():
"""TODO:
- Show account name & roles
- Count their assets with a query, link to their (new) list
"""
Basic information about the currently logged-in user.
Plus basic actions (logout, reset pwd)
"""
num_assets_in_account = GenericAsset.query.filter(
GenericAsset.account_id == current_user.account_id
).count()
return render_flexmeasures_template(
"admin/logged_in_user.html",
logged_in_user=current_user,
roles=",".join([role.name for role in current_user.roles]),
num_assets=len(get_assets()),
num_assets=num_assets_in_account,
)

0 comments on commit c8903b9

Please sign in to comment.