diff --git a/data/config/mosquitto/public/default-dynamic-security.json b/data/config/mosquitto/public/default-dynamic-security.json index a5e68fde77..372d91a918 100644 --- a/data/config/mosquitto/public/default-dynamic-security.json +++ b/data/config/mosquitto/public/default-dynamic-security.json @@ -70,7 +70,7 @@ "anonymousGroup": "anonymous", "roles": [ { - "rolename": "openwb-version:3", + "rolename": "openwb-version:4", "textname": "openWB Versionsnummer", "textdescription": "Diese Rolle ist ein Platzhalter für die openWB Versionsnummer und wird automatisch aktualisiert. Sie hat keine direkten Berechtigungen.", "acls": [] @@ -112,13 +112,13 @@ }, { "acltype": "publishClientReceive", - "topic": "openWB/general/internal_chargepoint/global_data", + "topic": "openWB/internal_chargepoint/global_data", "priority": 0, "allow": true }, { "acltype": "publishClientReceive", - "topic": "openWB/general/internal_chargepoint/+/data/parent_cp", + "topic": "openWB/internal_chargepoint/+/data/parent_cp", "priority": 0, "allow": true } @@ -1120,12 +1120,6 @@ "topic": "openWB/general/chargemode_config/pv_charging/switch_off_delay", "priority": 0, "allow": true - }, - { - "acltype": "publishClientReceive", - "topic": "openWB/general/chargemode_config/pv_charging/phases_to_use", - "priority": 0, - "allow": true } ] }, diff --git a/packages/control/general.py b/packages/control/general.py index b5a5ab9ba0..80941ead81 100644 --- a/packages/control/general.py +++ b/packages/control/general.py @@ -87,8 +87,6 @@ class GeneralData: extern_display_mode: str = field(default="primary", metadata={ "topic": "extern_display_mode"}) extern: bool = field(default=False, metadata={"topic": "extern"}) - external_buttons_hw: bool = field( - default=False, metadata={"topic": "external_buttons_hw"}) grid_protection_active: bool = field( default=False, metadata={"topic": "grid_protection_active"}) grid_protection_configured: bool = field( diff --git a/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py b/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py new file mode 100644 index 0000000000..b06822ab14 --- /dev/null +++ b/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py @@ -0,0 +1,193 @@ +import json +from pathlib import Path +import re + +from helpermodules.update_config import UpdateConfig +from test_utils.test_environment import running_on_github + + +def _get_data_path() -> Path: + if running_on_github(): + return Path("/home/runner/work/core/core/data") + else: + return Path(__file__).resolve().parents[3]/"data" + + +DEFAULT_DYNAMIC_SECURITY_PATH = _get_data_path()/"config/mosquitto/public/default-dynamic-security.json" +ROLE_TEMPLATES_PATH = _get_data_path()/"config/mosquitto/public/role-templates.json" + + +INTERNAL_TOPICS = ['openWB/bat/set/charging_power_left', + 'openWB/bat/set/regulate_up', + 'openWB/bat/+/set/power_limit', + 'openWB/command/max_id/autolock_plan', + 'openWB/command/max_id/charge_template', + 'openWB/command/max_id/charge_template_scheduled_plan', + 'openWB/command/max_id/charge_template_time_charging_plan', + 'openWB/command/max_id/chargepoint_template', + 'openWB/command/max_id/device', + 'openWB/command/max_id/ev_template', + 'openWB/command/max_id/hierarchy', + 'openWB/command/max_id/io_action', + 'openWB/command/max_id/io_device', + 'openWB/command/max_id/mqtt_bridge', + 'openWB/command/max_id/vehicle', + 'openWB/command/[A-Za-z0-9_]+/error', + 'openWB/command/todo', + 'openWB/counter/set/disengageable_smarthome_power', + 'openWB/counter/set/invalid_home_consumption', + 'openWB/counter/set/simulation', + 'openWB/counter/+/set/consumption_left', + 'openWB/counter/+/set/error_timer', + 'openWB/counter/+/set/released_surplus', + 'openWB/counter/+/set/reserved_surplus', + 'openWB/general/grid_protection_active', + 'openWB/general/grid_protection_timestamp', + 'openWB/general/grid_protection_random_stop', + 'openWB/general/price_kwh', # obsolete topic + 'openWB/graph/config/duration', + 'openWB/internal_chargepoint/+/data/cp_interruption_duration', + 'openWB/internal_chargepoint/+/data/set_current', + 'openWB/internal_chargepoint/+/data/phases_to_use', + 'openWB/internal_chargepoint/+/get/charge_state', + 'openWB/internal_chargepoint/+/get/currents', + 'openWB/internal_chargepoint/+/get/current_branch', + 'openWB/internal_chargepoint/+/get/current_commit', + 'openWB/internal_chargepoint/+/get/error_timestamp', + 'openWB/internal_chargepoint/+/get/evse_current', + 'openWB/internal_chargepoint/+/get/fault_state', + 'openWB/internal_chargepoint/+/get/fault_str', + 'openWB/internal_chargepoint/+/get/frequency', + 'openWB/internal_chargepoint/+/get/max_evse_current', + 'openWB/internal_chargepoint/+/get/plug_state', + 'openWB/internal_chargepoint/+/get/phases_in_use', + 'openWB/internal_chargepoint/+/get/exported', + 'openWB/internal_chargepoint/+/get/imported', + 'openWB/internal_chargepoint/+/get/power', + 'openWB/internal_chargepoint/+/get/powers', + 'openWB/internal_chargepoint/+/get/power_factors', + 'openWB/internal_chargepoint/+/get/vehicle_id', + 'openWB/internal_chargepoint/+/get/version', + 'openWB/internal_chargepoint/+/get/voltages', + 'openWB/internal_chargepoint/+/get/serial_number', + 'openWB/internal_chargepoint/+/get/soc', + 'openWB/internal_chargepoint/+/get/soc_timestamp', + 'openWB/internal_chargepoint/+/get/simulation', + 'openWB/internal_chargepoint/+/get/state_str', + 'openWB/internal_chargepoint/+/get/rfid', + 'openWB/internal_chargepoint/+/get/rfid_timestamp', + # abgesehen vom Fahrzeug können mqtt get-Topics bei aktiver BV nur gepublished werden + 'openWB/mqtt/bat/+/get/power', + 'openWB/mqtt/bat/+/get/soc', + 'openWB/mqtt/bat/+/get/imported', + 'openWB/mqtt/bat/+/get/exported', + 'openWB/mqtt/counter/+/get/currents', + 'openWB/mqtt/counter/+/get/imported', + 'openWB/mqtt/counter/+/get/exported', + 'openWB/mqtt/counter/+/get/power', + 'openWB/mqtt/counter/+/get/frequency', + 'openWB/mqtt/counter/+/get/power_factors', + 'openWB/mqtt/counter/+/get/powers', + 'openWB/mqtt/counter/+/get/voltages', + 'openWB/mqtt/inverter/+/get/currents', + 'openWB/mqtt/inverter/+/get/power', + 'openWB/mqtt/inverter/+/get/exported', + 'openWB/mqtt/inverter/+/get/dc_power', + 'openWB/set/log/request', + 'openWB/set/log/data', + 'openWB/optional/int_display/detected', + 'openWB/optional/int_display/pin_active', + 'openWB/optional/int_display/pin_code', + 'openWB/system/datastore_version', + 'openWB/system/device/+/component/+/simulation', + 'openWB/system/device/+/component/+/simulation/power_present', + 'openWB/system/device/+/component/+/simulation/present_exported', + 'openWB/system/device/+/component/+/simulation/present_imported', + 'openWB/system/device/+/component/+/simulation/timestamp_present', + 'openWB/system/device/module_update_completed', + 'openWB/system/hostname', + 'openWB/system/lastlivevaluesJson', + 'openWB/system/release_train'] + +NOT_PERSISTENT_TOPICS = ['openWB/system/messages/[^/]+', + 'openWB/system/messages/.*', + 'openWB/command/[^/]+/messages/[^/]+', + 'openWB/command/[^/]+/messages/.*', + 'openWB/system/serial_number', + 'openWB/system/mac_address', + 'openWB/optional/dc_charging', + 'others/.*', + '$CONTROL/dynamic-security/.*', + '$SYS/.*', + 'openWB/simpleAPI/set/.*', + 'openWB-remote/.*', + 'openWB/log/[^/]+/data', + 'openWB/log/daily/.*', + 'openWB/log/monthly/.*', + 'openWB/log/yearly/.*', + '$CONTROL/dynamic-security/v1', + '$CONTROL/dynamic-security/v1/response', + ] + + +def test_valid_topics_vs_role_topics(): + def parse_acl_topic_to_regex(acl_topic): + if "openWB/set" not in acl_topic and "#" != acl_topic and "openWB/#" != acl_topic: + # convert mqtt topic pattern to regex pattern + topic_regex = acl_topic.replace("+", "[^/]+").replace("#", ".*").replace("", "[^/]+") + return topic_regex + return None + # setup + valid_topics = list(UpdateConfig.valid_topic) + for i, valid_topic in enumerate(valid_topics): + if valid_topic.startswith("^"): + valid_topic = valid_topic[1:] + if valid_topic.endswith("$"): + valid_topic = valid_topic[:-1] + valid_topic = re.sub(r"\[0-9]\+", "+", valid_topic) + valid_topic = re.sub(r"\[0-1]", "+", valid_topic) + valid_topic = re.sub(r"\[0-9]", "+", valid_topic) + valid_topics[i] = valid_topic + + with open(DEFAULT_DYNAMIC_SECURITY_PATH, "r") as f: + security = json.load(f) + role_topics = [] + for role in security.get("roles", []): + for acl in role.get("acls", []): + if "topic" in acl: + topic_regex = parse_acl_topic_to_regex(acl["topic"]) + if topic_regex: + role_topics.append(topic_regex) + with open(ROLE_TEMPLATES_PATH, "r") as f: + roles = json.load(f) + for role in roles: + for acl in role.get("acls", []): + if "topic" in acl: + topic_regex = parse_acl_topic_to_regex(acl["topic"]) + if topic_regex: + role_topics.append(topic_regex) + # execution + # prüfen, ob alle valid_topics in den role_topics vorkommen, damit neue Topics nicht in der BV vergessen werden + missing_valid_topic = [] + for valid_topic in valid_topics: + found = False + for role_topic in role_topics: + if re.fullmatch(role_topic, valid_topic) or valid_topic in INTERNAL_TOPICS: + found = True + break + if not found: + missing_valid_topic.append(valid_topic) + # prüfen, ob alle role_topics in den valid_topics vorkommen, damit keine ungültigen Topics in der BV sind + missing_role_topic = [] + for role_topic in role_topics: + found = False + for valid_topic in valid_topics: + if re.fullmatch(role_topic, valid_topic) or role_topic in NOT_PERSISTENT_TOPICS: + found = True + break + if not found: + missing_role_topic.append(role_topic) + + # assertion + assert len(missing_valid_topic) == 0, f"Fehlende Topics in der Benutzerverwaltung: {missing_valid_topic}" + assert len(missing_role_topic) == 0, f"Obsolete Topics in der Benutzerverwaltung: {missing_role_topic}" diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index e3de04db83..f2ff2bfc23 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -759,8 +759,6 @@ def process_general_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, bool) elif "openWB/set/general/control_interval" in msg.topic: self._validate_value(msg, int, [(10, 10), (20, 20), (60, 60)]) - elif "openWB/set/general/external_buttons_hw" in msg.topic: - self._validate_value(msg, bool) elif "openWB/set/general/chargemode_config/unbalanced_load_limit" in msg.topic: self._validate_value(msg, int, [(10, 32)]) elif ("openWB/set/general/chargemode_config/unbalanced_load" in msg.topic or @@ -790,22 +788,12 @@ def process_general_topic(self, msg: mqtt.MQTTMessage): elif "openWB/set/general/chargemode_config/" in msg.topic and "/phases_to_use" in msg.topic: self._validate_value(msg, int, [(1, 1), (3, 3)]) elif ("openWB/set/general/grid_protection_configured" in msg.topic or - "openWB/set/general/grid_protection_active" in msg.topic or - "openWB/set/general/mqtt_bridge" in msg.topic): + "openWB/set/general/grid_protection_active" in msg.topic): self._validate_value(msg, bool) elif "openWB/set/general/grid_protection_timestamp" in msg.topic: self._validate_value(msg, float, [(0, TIMESTAMP_2100)]) elif "openWB/set/general/grid_protection_random_stop" in msg.topic: self._validate_value(msg, int, [(0, 90)]) - elif "openWB/set/general/notifications/selected" in msg.topic: - self._validate_value(msg, str) - elif "openWB/set/general/notifications/configuration" in msg.topic: - self._validate_value(msg, "json") - elif ("openWB/set/general/notifications/start_charging" in msg.topic or - "openWB/set/general/notifications/stop_charging" in msg.topic or - "openWB/set/general/notifications/plug" in msg.topic or - "openWB/set/general/notifications/smart_home" in msg.topic): - self._validate_value(msg, bool) elif ("openWB/set/general/prices/bat" in msg.topic or "openWB/set/general/prices/grid" in msg.topic or "openWB/set/general/prices/pv" in msg.topic): diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index c5a6c1bbaa..7ebe48c613 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -218,22 +218,14 @@ class UpdateConfig: "^openWB/general/extern_display_mode$", "^openWB/general/charge_log_data_config$", "^openWB/general/control_interval$", - "^openWB/general/external_buttons_hw$", "^openWB/general/grid_protection_configured$", "^openWB/general/grid_protection_active$", "^openWB/general/http_api$", "^openWB/general/modbus_control$", - "^openWB/general/mqtt_bridge$", "^openWB/general/grid_protection_timestamp$", "^openWB/general/grid_protection_random_stop$", "^openWB/general/range_unit$", "^openWB/general/temporary_charge_templates_active$", - "^openWB/general/notifications/selected$", - "^openWB/general/notifications/configuration$", - "^openWB/general/notifications/start_charging$", - "^openWB/general/notifications/stop_charging$", - "^openWB/general/notifications/plug$", - "^openWB/general/notifications/smart_home$", "^openWB/general/chargemode_config/unbalanced_load_limit$", "^openWB/general/chargemode_config/unbalanced_load$", "^openWB/general/chargemode_config/pv_charging/bat_mode$", @@ -263,7 +255,6 @@ class UpdateConfig: "^openWB/graph/lastlivevaluesJson$", "^openWB/internal_chargepoint/global_data$", - "^openWB/internal_chargepoint/global_config$", "^openWB/internal_chargepoint/[0-1]/data/cp_interruption_duration$", "^openWB/internal_chargepoint/[0-1]/data/set_current$", "^openWB/internal_chargepoint/[0-1]/data/phases_to_use$", @@ -298,11 +289,32 @@ class UpdateConfig: "^openWB/io/action/[0-9]+/config$", "^openWB/io/action/[0-9]+/timestamp$", + "^openWB/io/states/[0-9]+/get/analog_input$", + "^openWB/io/states/[0-9]+/get/analog_output$", + "^openWB/io/states/[0-9]+/get/digital_input$", + "^openWB/io/states/[0-9]+/get/digital_output$", + "^openWB/io/states/[0-9]+/set/analog_input$", + "^openWB/io/states/[0-9]+/set/analog_output$", + "^openWB/io/states/[0-9]+/set/digital_input$", + "^openWB/io/states/[0-9]+/set/digital_output$", "^openWB/mqtt/bat/[0-9]+/get/power$", "^openWB/mqtt/bat/[0-9]+/get/soc$", "^openWB/mqtt/bat/[0-9]+/get/imported$", "^openWB/mqtt/bat/[0-9]+/get/exported$", + "^openWB/mqtt/bat/[0-9]+/set/power_limit$", + "^openWB/mqtt/chargepoint/[0-9]+/set/current$", + "^openWB/mqtt/chargepoint/[0-9]+/set/phases_to_use$", + "^openWB/mqtt/chargepoint/[0-9]+/get/currents$", + "^openWB/mqtt/chargepoint/[0-9]+/get/power$", + "^openWB/mqtt/chargepoint/[0-9]+/get/plug_state$", + "^openWB/mqtt/chargepoint/[0-9]+/get/charge_state$", + "^openWB/mqtt/chargepoint/[0-9]+/get/phases_in_use$", + "^openWB/mqtt/chargepoint/[0-9]+/get/imported$", + "^openWB/mqtt/chargepoint/[0-9]+/get/exported$", + "^openWB/mqtt/chargepoint/[0-9]+/get/voltages$", + "^openWB/mqtt/chargepoint/[0-9]+/get/power_factors$", + "^openWB/mqtt/chargepoint/[0-9]+/get/rfid$", "^openWB/mqtt/counter/[0-9]+/get/currents$", "^openWB/mqtt/counter/[0-9]+/get/imported$", "^openWB/mqtt/counter/[0-9]+/get/exported$", @@ -322,6 +334,8 @@ class UpdateConfig: "^openWB/set/log/request", "^openWB/set/log/data", + "^openWB/optional/ep/configured$", + "^openWB/optional/ep/get/prices$", "^openWB/optional/ep/flexible_tariff/get/fault_state$", "^openWB/optional/ep/flexible_tariff/get/fault_str$", "^openWB/optional/ep/flexible_tariff/get/prices$", @@ -338,7 +352,7 @@ class UpdateConfig: "^openWB/optional/int_display/standby$", "^openWB/optional/int_display/rotation$", "^openWB/optional/int_display/theme$", - "^openWB/optional/int_display/only_local_charge_points", + "^openWB/optional/int_display/only_local_charge_points$", "^openWB/optional/monitoring/config$", "^openWB/optional/rfid/active$", "^openWB/optional/ocpp/config$", @@ -605,16 +619,9 @@ class UpdateConfig: ("openWB/general/control_interval", 10), ("openWB/general/extern", False), ("openWB/general/extern_display_mode", "primary"), - ("openWB/general/external_buttons_hw", False), ("openWB/general/grid_protection_configured", True), ("openWB/general/http_api", False), ("openWB/general/modbus_control", False), - ("openWB/general/notifications/selected", "none"), - ("openWB/general/notifications/plug", False), - ("openWB/general/notifications/start_charging", False), - ("openWB/general/notifications/stop_charging", False), - ("openWB/general/notifications/smart_home", False), - ("openWB/general/notifications/configuration", {}), ("openWB/general/prices/bat", Prices().bat), ("openWB/general/prices/grid", Prices().grid), ("openWB/general/prices/pv", Prices().pv),