From 5a815de617244e42d60eb4aef912665ce2a81cea Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 21 Jan 2026 11:56:01 +0100 Subject: [PATCH 1/3] update acls --- packages/helpermodules/mosquitto_dynsec.py | 142 ++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/packages/helpermodules/mosquitto_dynsec.py b/packages/helpermodules/mosquitto_dynsec.py index fa92d221ef..5173c975b0 100644 --- a/packages/helpermodules/mosquitto_dynsec.py +++ b/packages/helpermodules/mosquitto_dynsec.py @@ -1,7 +1,9 @@ from json import load as json_load +import json import logging from pathlib import Path -from typing import TypedDict, Optional +import re +from typing import Tuple, TypedDict, Optional from helpermodules.subdata import SubData from helpermodules.utils.run_command import run_command @@ -83,6 +85,7 @@ def remove_acl_role(role_template: str, id: int): def check_roles_at_start(): + update_acls() flag_path = Path(Path(__file__).resolve().parents[2]/"ramdisk"/"init_user_management") if flag_path.is_file(): with open(flag_path, "r") as file: @@ -104,3 +107,140 @@ def check_roles_at_start(): if "io" in key: add_acl_role("io-device--access", value.config.id) flag_path.unlink() + + +def _extract_id_from_role_name(role_name: str) -> Optional[int]: + numbers = re.findall(r'\d+', role_name) + if numbers: + return int(numbers[0]) + return None + + +def _compare_acl(template_acl: MosquittoAcl, configured_acl: MosquittoAcl) -> bool: + if template_acl["acltype"] == configured_acl["acltype"]: + if re.sub(r'/\d+/', '//', template_acl["topic"]) == re.sub(r'/\d+/', '//', configured_acl["topic"]): + if template_acl["allow"] == configured_acl["allow"]: + if template_acl["priority"] == configured_acl["priority"]: + return True + return False + + +def _get_configured_role_data(role_name: str) -> Optional[MosquittoRole]: + role_output = run_command([ + "mosquitto_ctrl", "dynsec", "getRole", role_name]) + # Parse the text output since it's not JSON + role_data = {"rolename": role_name, "acls": []} + lines = role_output.strip().split('\n') + for line in lines[1:]: # Skip first line with rolename + if "ACLs:" in line: + line = line.replace("ACLs:", "") + if line.strip() and ':' in line: + parts = [p.strip() for p in line.split(':')] + if len(parts) >= 4: + acl = { + "acltype": parts[0], + "allow": parts[1] == "allow", + "topic": parts[2].split('(')[0].strip(), + "priority": int(parts[3].replace(')', '').strip()) + } + role_data["acls"].append(acl) + return role_data + + +def update_acls(): + def is_version_updated() -> Tuple[str, str]: + for role in roles: + if "openwb-version" in role: + current_version = role.split(":")[1] + break + else: + raise RuntimeError("openwb-version role not found") + + for role in dynsec_config["roles"]: + try: + if "openwb-version" in role["rolename"]: + template_version = role["rolename"].split(":")[1] + break + except Exception: + continue + else: + raise RuntimeError("openwb-version role not found in default-dynamic-security.json") + + if current_version != template_version: + log.debug(f"Updating ACLs from version {current_version} to {template_version}") + return current_version, template_version + + def get_template_role_data() -> Optional[MosquittoRole]: + template_role_data = None + for config_role in dynsec_config["roles"]: + if (config_role["rolename"] == role_data["rolename"] or + ("openwb-version" in config_role["rolename"] and "openwb-version" in role_data["rolename"])): + template_role_data = config_role + break + else: + for config_role in role_templates_config: + pattern = config_role["rolename"].replace("", r"\d+") + if re.match(pattern, role_data["rolename"]): + template_role_data = config_role + break + else: + raise RuntimeError(f"Role {role_data['rolename']} not found in default-dynamic-security.json") + return template_role_data + + try: + roles = _list_acl_roles() + with open(_get_packages_path()/"data/config/mosquitto/public/default-dynamic-security.json", "r") as f: + dynsec_config = json.load(f) + current_version, template_version = is_version_updated() + if current_version != template_version: + with open(_get_packages_path()/"data/config/mosquitto/public/role-templates.json", "r") as f: + role_templates_config = json.load(f) + for role_name in roles: + try: + role_data = _get_configured_role_data(role_name) + template_role_data = get_template_role_data() + if template_role_data: + # vegleiche die zwei ACL dicts, welche ACLs hinzugefügt, geändert oder entfernt wurden + for acl in role_data["acls"]: + for template_acl in template_role_data["acls"]: + if _compare_acl(template_acl, acl): + break + else: + log.debug(f"ACL {acl['acltype']}:{'allow' if acl['allow'] else 'deny'}:{acl['topic']}:" + f"{acl['priority']} in Rolle {role_data['rolename']} wird entfernt.") + run_command([ + "mosquitto_ctrl", "dynsec", "removeRoleAcl", role_data["rolename"], + acl["acltype"], acl["topic"] + ]) + for acl in template_role_data["acls"]: + for role_acl in role_data["acls"]: + if _compare_acl(acl, role_acl): + break + else: + rolename_id = _extract_id_from_role_name(role_data["rolename"]) + if rolename_id is not None: + acl["topic"] = acl["topic"].replace("", str(rolename_id)) + log.debug(f"ACL {acl['acltype']}:{'allow' if acl['allow'] else 'deny'}:{acl['topic']}:" + f"{acl['priority']} in Rolle {role_data['rolename']} wird hinzugefügt.") + run_command([ + "mosquitto_ctrl", "dynsec", "addRoleAcl", role_data["rolename"], + acl["acltype"], acl["topic"], + "allow" if acl["allow"] else "deny", + str(acl["priority"]) + ]) + elif template_role_data is None: + log.debug(f"Rolle '{role_data['rolename']}' existiert nicht in den Konfigurationsdateien und" + " wird gelöscht.") + run_command(["mosquitto_ctrl", "dynsec", "deleteRole", role_data["rolename"]]) + except Exception: + log.exception(f"Fehler beim Aktualisieren der Rolle '{role_name}'") + # bei allen anderen Rollen dürfen nur die ACLs editiert werden, + # damit diese in den Benutzergruppen erhalten bleiben + run_command(["mosquitto_ctrl", "dynsec", "deleteRole", f"openwb-version:{current_version}"]) + run_command(["mosquitto_ctrl", "dynsec", "createRole", f"openwb-version:{template_version}"]) + except Exception: + log.exception("Fehler beim Aktualisieren der ACLs") + + +def _get_packages_path() -> Path: + return Path(__file__).resolve().parents[2] From 910020527b13c974638720536e99723ec538e060 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 23 Jan 2026 09:25:48 +0100 Subject: [PATCH 2/3] review --- packages/helpermodules/mosquitto_dynsec.py | 58 ++++++++++------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/packages/helpermodules/mosquitto_dynsec.py b/packages/helpermodules/mosquitto_dynsec.py index 5173c975b0..3402e17a49 100644 --- a/packages/helpermodules/mosquitto_dynsec.py +++ b/packages/helpermodules/mosquitto_dynsec.py @@ -17,6 +17,14 @@ class MosquittoAcl(TypedDict): allow: bool priority: int + def __eq__(self, other): + if self["acltype"] == other["acltype"]: + if re.sub(r'/\d+/', '//', self["topic"]) == re.sub(r'/\d+/', '//', other["topic"]): + if self["allow"] == other["allow"]: + if self["priority"] == other["priority"]: + return True + return False + class MosquittoRole(TypedDict): rolename: str @@ -116,15 +124,6 @@ def _extract_id_from_role_name(role_name: str) -> Optional[int]: return None -def _compare_acl(template_acl: MosquittoAcl, configured_acl: MosquittoAcl) -> bool: - if template_acl["acltype"] == configured_acl["acltype"]: - if re.sub(r'/\d+/', '//', template_acl["topic"]) == re.sub(r'/\d+/', '//', configured_acl["topic"]): - if template_acl["allow"] == configured_acl["allow"]: - if template_acl["priority"] == configured_acl["priority"]: - return True - return False - - def _get_configured_role_data(role_name: str) -> Optional[MosquittoRole]: role_output = run_command([ "mosquitto_ctrl", "dynsec", "getRole", role_name]) @@ -147,10 +146,13 @@ def _get_configured_role_data(role_name: str) -> Optional[MosquittoRole]: return role_data +VERSION_STRING = "openwb-version:" + + def update_acls(): - def is_version_updated() -> Tuple[str, str]: + def get_acl_versions() -> Tuple[str, str]: for role in roles: - if "openwb-version" in role: + if VERSION_STRING in role: current_version = role.split(":")[1] break else: @@ -158,7 +160,7 @@ def is_version_updated() -> Tuple[str, str]: for role in dynsec_config["roles"]: try: - if "openwb-version" in role["rolename"]: + if VERSION_STRING in role["rolename"]: template_version = role["rolename"].split(":")[1] break except Exception: @@ -167,31 +169,25 @@ def is_version_updated() -> Tuple[str, str]: raise RuntimeError("openwb-version role not found in default-dynamic-security.json") if current_version != template_version: - log.debug(f"Updating ACLs from version {current_version} to {template_version}") + log.debug(f"Current ACL version: '{current_version}' Template version: '{template_version}'") return current_version, template_version def get_template_role_data() -> Optional[MosquittoRole]: - template_role_data = None for config_role in dynsec_config["roles"]: if (config_role["rolename"] == role_data["rolename"] or - ("openwb-version" in config_role["rolename"] and "openwb-version" in role_data["rolename"])): - template_role_data = config_role - break - else: - for config_role in role_templates_config: - pattern = config_role["rolename"].replace("", r"\d+") - if re.match(pattern, role_data["rolename"]): - template_role_data = config_role - break - else: - raise RuntimeError(f"Role {role_data['rolename']} not found in default-dynamic-security.json") - return template_role_data + (VERSION_STRING in config_role["rolename"] and VERSION_STRING in role_data["rolename"])): + return config_role + for config_role in role_templates_config: + pattern = config_role["rolename"].replace("", r"\d+") + if re.match(pattern, role_data["rolename"]): + return config_role + raise RuntimeError(f"Role {role_data['rolename']} not found in default-dynamic-security.json") try: roles = _list_acl_roles() with open(_get_packages_path()/"data/config/mosquitto/public/default-dynamic-security.json", "r") as f: dynsec_config = json.load(f) - current_version, template_version = is_version_updated() + current_version, template_version = get_acl_versions() if current_version != template_version: with open(_get_packages_path()/"data/config/mosquitto/public/role-templates.json", "r") as f: role_templates_config = json.load(f) @@ -203,7 +199,7 @@ def get_template_role_data() -> Optional[MosquittoRole]: # vegleiche die zwei ACL dicts, welche ACLs hinzugefügt, geändert oder entfernt wurden for acl in role_data["acls"]: for template_acl in template_role_data["acls"]: - if _compare_acl(template_acl, acl): + if template_acl == acl: break else: log.debug(f"ACL {acl['acltype']}:{'allow' if acl['allow'] else 'deny'}:{acl['topic']}:" @@ -214,7 +210,7 @@ def get_template_role_data() -> Optional[MosquittoRole]: ]) for acl in template_role_data["acls"]: for role_acl in role_data["acls"]: - if _compare_acl(acl, role_acl): + if acl == role_acl: break else: rolename_id = _extract_id_from_role_name(role_data["rolename"]) @@ -236,8 +232,8 @@ def get_template_role_data() -> Optional[MosquittoRole]: log.exception(f"Fehler beim Aktualisieren der Rolle '{role_name}'") # bei allen anderen Rollen dürfen nur die ACLs editiert werden, # damit diese in den Benutzergruppen erhalten bleiben - run_command(["mosquitto_ctrl", "dynsec", "deleteRole", f"openwb-version:{current_version}"]) - run_command(["mosquitto_ctrl", "dynsec", "createRole", f"openwb-version:{template_version}"]) + run_command(["mosquitto_ctrl", "dynsec", "deleteRole", f"{VERSION_STRING}{current_version}"]) + run_command(["mosquitto_ctrl", "dynsec", "createRole", f"{VERSION_STRING}{template_version}"]) except Exception: log.exception("Fehler beim Aktualisieren der ACLs") From 271b7a626e42aa66ce2f531291774c0c280ff32a Mon Sep 17 00:00:00 2001 From: benderl Date: Fri, 23 Jan 2026 12:53:00 +0100 Subject: [PATCH 3/3] Update mosquitto_dynsec.py add comments --- packages/helpermodules/mosquitto_dynsec.py | 62 +++++++++++----------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/helpermodules/mosquitto_dynsec.py b/packages/helpermodules/mosquitto_dynsec.py index 3402e17a49..a0db2a2a9e 100644 --- a/packages/helpermodules/mosquitto_dynsec.py +++ b/packages/helpermodules/mosquitto_dynsec.py @@ -195,39 +195,41 @@ def get_template_role_data() -> Optional[MosquittoRole]: try: role_data = _get_configured_role_data(role_name) template_role_data = get_template_role_data() - if template_role_data: - # vegleiche die zwei ACL dicts, welche ACLs hinzugefügt, geändert oder entfernt wurden - for acl in role_data["acls"]: - for template_acl in template_role_data["acls"]: - if template_acl == acl: - break - else: - log.debug(f"ACL {acl['acltype']}:{'allow' if acl['allow'] else 'deny'}:{acl['topic']}:" - f"{acl['priority']} in Rolle {role_data['rolename']} wird entfernt.") - run_command([ - "mosquitto_ctrl", "dynsec", "removeRoleAcl", role_data["rolename"], - acl["acltype"], acl["topic"] - ]) - for acl in template_role_data["acls"]: - for role_acl in role_data["acls"]: - if acl == role_acl: - break - else: - rolename_id = _extract_id_from_role_name(role_data["rolename"]) - if rolename_id is not None: - acl["topic"] = acl["topic"].replace("", str(rolename_id)) - log.debug(f"ACL {acl['acltype']}:{'allow' if acl['allow'] else 'deny'}:{acl['topic']}:" - f"{acl['priority']} in Rolle {role_data['rolename']} wird hinzugefügt.") - run_command([ - "mosquitto_ctrl", "dynsec", "addRoleAcl", role_data["rolename"], - acl["acltype"], acl["topic"], - "allow" if acl["allow"] else "deny", - str(acl["priority"]) - ]) - elif template_role_data is None: + # entferne Rollen, die in der Konfigurationsdatei nicht vorhanden sind + if template_role_data is None: log.debug(f"Rolle '{role_data['rolename']}' existiert nicht in den Konfigurationsdateien und" " wird gelöscht.") run_command(["mosquitto_ctrl", "dynsec", "deleteRole", role_data["rolename"]]) + + # entferne ACLs aus der Rolle, wenn diese im Template nicht vorhanden sind + for acl in role_data["acls"]: + for template_acl in template_role_data["acls"]: + if template_acl == acl: + break + else: + log.debug(f"ACL {acl['acltype']}:{'allow' if acl['allow'] else 'deny'}:{acl['topic']}:" + f"{acl['priority']} in Rolle {role_data['rolename']} wird entfernt.") + run_command([ + "mosquitto_ctrl", "dynsec", "removeRoleAcl", role_data["rolename"], + acl["acltype"], acl["topic"] + ]) + # ergänze zusätzliche ACLs aus dem Template in der Rollen + for acl in template_role_data["acls"]: + for role_acl in role_data["acls"]: + if acl == role_acl: + break + else: + rolename_id = _extract_id_from_role_name(role_data["rolename"]) + if rolename_id is not None: + acl["topic"] = acl["topic"].replace("", str(rolename_id)) + log.debug(f"ACL {acl['acltype']}:{'allow' if acl['allow'] else 'deny'}:{acl['topic']}:" + f"{acl['priority']} in Rolle {role_data['rolename']} wird hinzugefügt.") + run_command([ + "mosquitto_ctrl", "dynsec", "addRoleAcl", role_data["rolename"], + acl["acltype"], acl["topic"], + "allow" if acl["allow"] else "deny", + str(acl["priority"]) + ]) except Exception: log.exception(f"Fehler beim Aktualisieren der Rolle '{role_name}'") # bei allen anderen Rollen dürfen nur die ACLs editiert werden,