File firewalld-normalize-new-rich-rules-before-comparing-.patch of Package salt

From 522b2331e6584758aeaefbf2d41f0c18cd1113d9 Mon Sep 17 00:00:00 2001
From: Marek Czernek <marek.czernek@suse.com>
Date: Tue, 23 Jul 2024 13:01:27 +0200
Subject: [PATCH] firewalld: normalize new rich rules before comparing
 to old (bsc#1222684) (#648)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Normalize new rich rules before comparing to old

Firewallcmd rich rule output quotes each
assigned part of the rich rule, for example:
rule family="ipv4" source port port="161" ...
The firewalld module must first normalize
the user defined rich rules to match the
firewallcmd output before comparison to
ensure idempotency.

* Add changelog entry

* Enhance documentation for normalization function

* Add unit tests to cover rich rules normalization

---------

Co-authored-by: Pablo Suárez Hernández <psuarezhernandez@suse.com>
---
 changelog/61235.fixed.md                    |  1 +
 salt/states/firewalld.py                    | 38 +++++++++++-
 tests/pytests/unit/states/test_firewalld.py | 64 +++++++++++++++++++++
 3 files changed, 102 insertions(+), 1 deletion(-)
 create mode 100644 changelog/61235.fixed.md
 create mode 100644 tests/pytests/unit/states/test_firewalld.py

diff --git a/changelog/61235.fixed.md b/changelog/61235.fixed.md
new file mode 100644
index 00000000000..7ae9bb40800
--- /dev/null
+++ b/changelog/61235.fixed.md
@@ -0,0 +1 @@
+- firewalld: normalize new rich rules before comparing to old ones
diff --git a/salt/states/firewalld.py b/salt/states/firewalld.py
index 534b9dd62df..9ce0bfc61a8 100644
--- a/salt/states/firewalld.py
+++ b/salt/states/firewalld.py
@@ -204,7 +204,6 @@ def present(
     rich_rules=None,
     prune_rich_rules=False,
 ):
-
     """
     Ensure a zone has specific attributes.
 
@@ -378,6 +377,42 @@ def service(name, ports=None, protocols=None):
     return ret
 
 
+def _normalize_rich_rules(rich_rules):
+    """
+    Make sure rich rules are normalized and attributes
+    are quoted with double quotes so it matches the output
+    from firewall-cmd
+
+    Example:
+
+    rule family="ipv4" source address="192.168.0.0/16" port port=22 protocol=tcp accept
+    rule family="ipv4" source address="192.168.0.0/16" port port='22' protocol=tcp accept
+    rule family='ipv4' source address='192.168.0.0/16' port port='22' protocol=tcp accept
+
+    normalized to:
+
+    rule family="ipv4" source address="192.168.0.0/16" port port="22" protocol="tcp" accept
+    """
+    normalized_rules = []
+    for rich_rule in rich_rules:
+        normalized_rule = ""
+        for cmd in rich_rule.split(" "):
+            cmd_components = cmd.split("=", 1)
+            if len(cmd_components) == 2:
+                assigned_component = cmd_components[1]
+                if not assigned_component.startswith(
+                    '"'
+                ) and not assigned_component.endswith('"'):
+                    if assigned_component.startswith(
+                        "'"
+                    ) and assigned_component.endswith("'"):
+                        assigned_component = assigned_component[1:-1]
+                    cmd_components[1] = f'"{assigned_component}"'
+            normalized_rule = f"{normalized_rule} {'='.join(cmd_components)}"
+        normalized_rules.append(normalized_rule.lstrip())
+    return normalized_rules
+
+
 def _present(
     name,
     block_icmp=None,
@@ -761,6 +796,7 @@ def _present(
 
     if rich_rules or prune_rich_rules:
         rich_rules = rich_rules or []
+        rich_rules = _normalize_rich_rules(rich_rules)
         try:
             _current_rich_rules = __salt__["firewalld.get_rich_rules"](
                 name, permanent=True
diff --git a/tests/pytests/unit/states/test_firewalld.py b/tests/pytests/unit/states/test_firewalld.py
new file mode 100644
index 00000000000..0cbc59633bf
--- /dev/null
+++ b/tests/pytests/unit/states/test_firewalld.py
@@ -0,0 +1,64 @@
+"""
+    :codeauthor: Hristo Voyvodov <hristo.voyvodov@redsift.io>
+"""
+
+import pytest
+
+import salt.states.firewalld as firewalld
+from tests.support.mock import MagicMock, patch
+
+
+@pytest.fixture
+def configure_loader_modules():
+    return {firewalld: {"__opts__": {"test": False}}}
+
+
+@pytest.mark.parametrize(
+    "rich_rule",
+    [
+        (
+            [
+                'rule family="ipv4" source address="192.168.0.0/16" port port=22 protocol=tcp accept'
+            ]
+        ),
+        (
+            [
+                'rule family="ipv4" source address="192.168.0.0/16" port port=\'22\' protocol=tcp accept'
+            ]
+        ),
+        (
+            [
+                "rule family='ipv4' source address='192.168.0.0/16' port port='22' protocol=tcp accept"
+            ]
+        ),
+    ],
+)
+def test_present_rich_rules_normalized(rich_rule):
+    firewalld_reload_rules = MagicMock(return_value={})
+    firewalld_rich_rules = [
+        'rule family="ipv4" source address="192.168.0.0/16" port port="22" protocol="tcp" accept',
+    ]
+
+    firewalld_get_zones = MagicMock(
+        return_value=[
+            "block",
+            "public",
+        ]
+    )
+    firewalld_get_masquerade = MagicMock(return_value=False)
+    firewalld_get_rich_rules = MagicMock(return_value=firewalld_rich_rules)
+
+    __salt__ = {
+        "firewalld.reload_rules": firewalld_reload_rules,
+        "firewalld.get_zones": firewalld_get_zones,
+        "firewalld.get_masquerade": firewalld_get_masquerade,
+        "firewalld.get_rich_rules": firewalld_get_rich_rules,
+    }
+    with patch.dict(firewalld.__dict__, {"__salt__": __salt__}):
+        ret = firewalld.present("public", rich_rules=rich_rule)
+        assert ret == {
+            "changes": {},
+            "result": True,
+            "comment": "'public' is already in the desired state.",
+            "name": "public",
+        }
-- 
2.45.2


openSUSE Build Service is sponsored by