From 3b19a0da998e821eea60632f0a3dd42a10574acb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:30:54 +0000 Subject: [PATCH 1/3] Initial plan From 37ed4089ef80b70e1f08fe6e46a48b0bbd064bbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:42:35 +0000 Subject: [PATCH 2/3] Check read permissions for regressors in trigger_forecast endpoint Agent-Logs-Url: https://github.com/FlexMeasures/flexmeasures/sessions/9b3ff676-cf08-4d79-8cd1-657ef1581d40 Co-authored-by: BelhsanHmida <149331360+BelhsanHmida@users.noreply.github.com> --- flexmeasures/api/v3_0/sensors.py | 13 +++ .../api/v3_0/tests/test_forecasting_api.py | 80 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 62a4273218..4e345e4f3a 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -1632,6 +1632,19 @@ def trigger_forecast(self, id: int, **params): # Put the sensor to save in the parameters parameters["sensor"] = params["sensor_to_save"].id + # Check read permissions for regressor sensors specified in the config. + # The schema has already validated that these sensor IDs exist. + config = parameters.get("config", {}) + regressor_ids = set( + config.get("future-regressors", []) + + config.get("past-regressors", []) + + config.get("regressors", []) + ) + for regressor_id in regressor_ids: + regressor = db.session.get(Sensor, regressor_id) + if regressor is not None: + check_access(regressor, "read") + # Set forecaster model model = parameters.pop("model", "TrainPredictPipeline") diff --git a/flexmeasures/api/v3_0/tests/test_forecasting_api.py b/flexmeasures/api/v3_0/tests/test_forecasting_api.py index df7e3fef8c..1b7fbbf6b4 100644 --- a/flexmeasures/api/v3_0/tests/test_forecasting_api.py +++ b/flexmeasures/api/v3_0/tests/test_forecasting_api.py @@ -1,3 +1,5 @@ +from datetime import timedelta + from flask import current_app import isodate import pytest @@ -8,6 +10,9 @@ from flexmeasures.api.tests.utils import get_auth_token from flexmeasures.data.services.forecasting import handle_forecasting_exception from flexmeasures.data.models.forecasting.pipelines import TrainPredictPipeline +from flexmeasures.data import db +from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType +from flexmeasures.data.models.time_series import Sensor @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True) @@ -126,3 +131,78 @@ def test_trigger_and_fetch_forecasts( # API should return exactly these most-recent beliefs assert api_forecasts == expected_values + + +@pytest.mark.parametrize( + "regressor_field", + ["future-regressors", "past-regressors", "regressors"], +) +@pytest.mark.parametrize( + "requesting_user", ["test_supplier_user_4@seita.nl"], indirect=True +) +def test_trigger_forecast_with_unreadable_regressor_returns_403( + app, + setup_roles_users_fresh_db, + setup_accounts_fresh_db, + requesting_user, + regressor_field, +): + """Triggering a forecast that uses a regressor the requesting user cannot read must return 403.""" + + supplier_account = setup_accounts_fresh_db["Supplier"] + prosumer_account = setup_accounts_fresh_db["Prosumer"] + + asset_type = GenericAssetType(name=f"test-asset-type-{regressor_field}") + db.session.add(asset_type) + + # Target sensor: owned by Supplier account – requesting user has create-children here + supplier_asset = GenericAsset( + name=f"supplier-target-asset-{regressor_field}", + generic_asset_type=asset_type, + owner=supplier_account, + ) + db.session.add(supplier_asset) + target_sensor = Sensor( + name=f"supplier-target-sensor-{regressor_field}", + unit="kW", + event_resolution=timedelta(hours=1), + generic_asset=supplier_asset, + ) + db.session.add(target_sensor) + + # Regressor sensor: owned by Prosumer account – requesting user has no read access here + prosumer_asset = GenericAsset( + name=f"prosumer-private-regressor-asset-{regressor_field}", + generic_asset_type=asset_type, + owner=prosumer_account, + ) + db.session.add(prosumer_asset) + private_regressor = Sensor( + name=f"prosumer-private-regressor-sensor-{regressor_field}", + unit="kW", + event_resolution=timedelta(hours=1), + generic_asset=prosumer_asset, + ) + db.session.add(private_regressor) + db.session.commit() + + client = app.test_client() + token = get_auth_token(client, "test_supplier_user_4@seita.nl", "testtest") + + payload = { + "start": "2025-01-05T00:00:00+00:00", + "end": "2025-01-05T02:00:00+00:00", + "max-forecast-horizon": "PT1H", + "forecast-frequency": "PT1H", + "config": { + "train-start": "2025-01-01T00:00:00+00:00", + "retrain-frequency": "PT1H", + regressor_field: [private_regressor.id], + }, + } + + trigger_url = url_for("SensorAPI:trigger_forecast", id=target_sensor.id) + trigger_res = client.post( + trigger_url, json=payload, headers={"Authorization": token} + ) + assert trigger_res.status_code == 403 From a52646658fcfb10dc2d47c64a5bd6c876a3d9ce6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:45:11 +0000 Subject: [PATCH 3/3] Remove defensive None guard (schema already validates sensor IDs), simplify asset type name Agent-Logs-Url: https://github.com/FlexMeasures/flexmeasures/sessions/9b3ff676-cf08-4d79-8cd1-657ef1581d40 Co-authored-by: BelhsanHmida <149331360+BelhsanHmida@users.noreply.github.com> --- flexmeasures/api/v3_0/sensors.py | 3 +-- flexmeasures/api/v3_0/tests/test_forecasting_api.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 4e345e4f3a..c5d2e31b1d 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -1642,8 +1642,7 @@ def trigger_forecast(self, id: int, **params): ) for regressor_id in regressor_ids: regressor = db.session.get(Sensor, regressor_id) - if regressor is not None: - check_access(regressor, "read") + check_access(regressor, "read") # Set forecaster model model = parameters.pop("model", "TrainPredictPipeline") diff --git a/flexmeasures/api/v3_0/tests/test_forecasting_api.py b/flexmeasures/api/v3_0/tests/test_forecasting_api.py index 1b7fbbf6b4..fddc8288ce 100644 --- a/flexmeasures/api/v3_0/tests/test_forecasting_api.py +++ b/flexmeasures/api/v3_0/tests/test_forecasting_api.py @@ -152,7 +152,7 @@ def test_trigger_forecast_with_unreadable_regressor_returns_403( supplier_account = setup_accounts_fresh_db["Supplier"] prosumer_account = setup_accounts_fresh_db["Prosumer"] - asset_type = GenericAssetType(name=f"test-asset-type-{regressor_field}") + asset_type = GenericAssetType(name="test-asset-type-regressor-perm") db.session.add(asset_type) # Target sensor: owned by Supplier account – requesting user has create-children here