URL: https://github.com/freeipa/freeipa/pull/2542
Author: t-woerner
 Title: #2542: Enable firewall in the tests
Action: opened

PR body:
"""
Currently the firewall is not enabled in the tests. The goal is to enable the 
file firewall using firewalld in the tests.

All uses of iptables and ip6tables commands in the tests need to be replaced.
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/2542/head:pr2542
git checkout pr2542
From 25796204c90f77cb2245b119e56c212afc185513 Mon Sep 17 00:00:00 2001
From: Thomas Woerner <[email protected]>
Date: Thu, 8 Nov 2018 13:45:56 +0100
Subject: [PATCH 1/4] New firewall support class in
 ipatests/pytest_ipa/integration/firewall

The new Firewall class provides methods to enable and disable a service,
service lists and also methods to apply a passthrough rule, also to add,
prepend and also remove a list of passthrough rules:

class Firewall
    __init__(host)
        Initialize with host where firewall changes should be applied

    enable_service(service)
        Enable firewall service in firewalld runtime and permanent
        environment

    disable_service(service)
        Disable firewall service in firewalld runtime and permanent
        environment

    enable_services(services)
        Enable list of firewall services in firewalld runtime and
        permanent environment

    disable_services(services)
        Disable list of firewall services in firewalld runtime and
        permanent environment

    passthrough_rule(rule, ipvs=["ipv4","ipv6"])
        Generic method to get direct passthrough rules to firewalld
        rule is an ip[6]tables rule without using the ip[6]tables command.
        The rule will per default be added to the IPv4 and IPv6 firewall.
        If there are IP version specific parts in the rule, please make
        sure that ipv is adapted properly.
        The rule is added to the direct sub chain of the chain that is
        used in the rule

    add_passthrough_rules(rules, ipvs=["ipv4","ipv6"])
        Add passthough rules to the end of the chain
        rules is a list of ip[6]tables rules, where the first entry of each
        rule is the chain. No --append/-A, --delete/-D should be added
        before the chain name, beacuse these are added by the method.
        If there are IP version specific parts in the rule, please make
        sure that ipv is adapted properly.

    prepend_passthrough_rules(rules, ipvs=["ipv4","ipv6"])
        Insert passthough rules starting at position 1 as a block
        rules is a list of ip[6]tables rules, where the first entry of each
        rule is the chain. No --append/-A, --delete/-D should be added
        before the chain name, beacuse these are added by the method.
        If there are IP version specific parts in the rule, please make
        sure that ipv is adapted properly.

    remove_passthrough_rules(rules, ipvs=["ipv4","ipv6"])
        Remove passthrough rules
        rules is a list of ip[6]tables rules, where the first entry of each
        rule is the chain. No --append/-A, --delete/-D should be added
        before the chain name, beacuse these are added by the method.
        If there are IP version specific parts in the rule, please make
        sure that ipv is adapted properly.

See: https://pagure.io/freeipa/issue/7755
Signed-off-by: Thomas Woerner <[email protected]>
---
 ipatests/pytest_ipa/integration/firewall.py | 123 ++++++++++++++++++++
 1 file changed, 123 insertions(+)
 create mode 100644 ipatests/pytest_ipa/integration/firewall.py

diff --git a/ipatests/pytest_ipa/integration/firewall.py b/ipatests/pytest_ipa/integration/firewall.py
new file mode 100644
index 0000000000..0eb94f6ef5
--- /dev/null
+++ b/ipatests/pytest_ipa/integration/firewall.py
@@ -0,0 +1,123 @@
+# Authors:
+#   Thomas Woerner <[email protected]>
+#
+# Copyright (C) 2018  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Firewall class for integration testing using firewalld"""
+
+from __future__ import absolute_import
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class Firewall:
+    def __init__(self, host):
+        """Initialize with host where firewall changes should be applied"""
+        self.host = host
+
+    def _rp_action(self, args):
+        """Run-time and permanant firewall action"""
+        # Run-time part
+        try:
+            self.host.run_command(args)
+        except Exception as e:
+            logger.error(str(e))
+        # Permanent part
+        try:
+            self.host.run_command(args + "--permament")
+        except Exception as e:
+            logger.error(str(e))
+
+    def enable_service(self, service):
+        """Enable firewall service in firewalld runtime and permanent
+        environment"""
+        logger.debug("Enabling service %s", service)
+        self._rp_action(["firewall-cmd", "--add-service", service])
+
+    def disable_service(self, service):
+        """Disable firewall service in firewalld runtime and permanent
+        environment"""
+        logger.debug("Disabling service %s", service)
+        self._rp_action(["firewall-cmd", "--remove-service", service])
+
+    def enable_services(self, services):
+        """Enable list of firewall services in firewalld runtime and
+        permanent environment"""
+        logger.debug("Enabling services %s", ",".join(services))
+        self._rp_action(["firewall-cmd", "--add-service",
+                         "{" + ",".join(services) + "}"])
+
+    def disable_services(self, services):
+        """Disable list of firewall services in firewalld runtime and
+        permanent environment"""
+        logger.debug("Disabling services %s", ",".join(services))
+        self._rp_action(["firewall-cmd", "--remove-service",
+                        "{" + ",".join(services) + "}"])
+
+    def passthrough_rule(self, rule, ipvs=["ipv4", "ipv6"]):
+        """Generic method to get direct passthrough rules to firewalld
+        rule is an ip[6]tables rule without using the ip[6]tables command.
+        The rule will per default be added to the IPv4 and IPv6 firewall.
+        If there are IP version specific parts in the rule, please make sure
+        that ipv is adapted properly.
+        The rule is added to the direct sub chain of the chain that is used
+        in the rule"""
+        logger.debug("Passthrough rule %s", ",".join(rule))
+        for ipv in ipvs:
+            args = ["firewall-cmd", "--direct", "--passthrough", ipv] + rule
+            try:
+                self.host.run_command(args)
+            except Exception as e:
+                logger.error(str(e))
+
+    def add_passthrough_rules(self, rules, ipvs=["ipv4", "ipv6"]):
+        """Add passthough rules to the end of the chain
+        rules is a list of ip[6]tables rules, where the first entry of each
+        rule is the chain. No --append/-A, --delete/-D should be added before
+        the chain name, beacuse these are added by the method.
+        If there are IP version specific parts in the rule, please make sure
+        that ipv is adapted properly.
+        """
+        for rule in rules:
+            self.passthrough_rule(["-A"] + rule, ipvs)
+
+    def prepend_passthrough_rules(self, rules, ipvs=["ipv4", "ipv6"]):
+        """Insert passthough rules starting at position 1 as a block
+        rules is a list of ip[6]tables rules, where the first entry of each
+        rule is the chain. No --append/-A, --delete/-D should be added before
+        the chain name, beacuse these are added by the method.
+        If there are IP version specific parts in the rule, please make sure
+        that ipv is adapted properly.
+        """
+        # first rule number in iptables is 1
+        i = 1
+        for rule in rules:
+            self.passthrough_rule(["-I", rule[0], str(i)] + rule[1:], ipvs)
+            i += 1
+
+    def remove_passthrough_rules(self, rules, ipvs=["ipv4", "ipv6"]):
+        """Remove passthrough rules
+        rules is a list of ip[6]tables rules, where the first entry of each
+        rule is the chain. No --append/-A, --delete/-D should be added before
+        the chain name, beacuse these are added by the method.
+        If there are IP version specific parts in the rule, please make sure
+        that ipv is adapted properly.
+        """
+        for rule in rules:
+            self.passthrough_rule(["-D"] + rule, ipvs)

From bbf475da0973338b8b3d92438990506dec3b60a2 Mon Sep 17 00:00:00 2001
From: Thomas Woerner <[email protected]>
Date: Thu, 8 Nov 2018 14:49:31 +0100
Subject: [PATCH 2/4] ipatests/pytest_ipa/integration/tasks.py: Configure
 firewall

install_master: Enable firewall services freeipa-ldap and freeipa-ldaps by
default, enable dns if setup_dns is set and enable freeipa-trust if
setup_adtrust is set. The services are enabled after the master has been
successfully installed.

install_replica: Enable firewall services freeipa-ldap, freeipa-ldaps and
freeipa-replication by default, enable dns if setup_dns is set and enable
freeipa-trust if setup_adtrust is set. The services are enabled before
the replica gets installed and disabled if the installation failed.

install_adtrust: Enable firewall service freeipa-trust after
ipa-adtrust-install has been called.

uninstall_master: Disable services freeipa-ldap, freeipa-ldaps,
freeipa-trust, freeipa-replication and dns after
ipa-server-install --uninstall -U has been called.

See: https://pagure.io/freeipa/issue/7755
Signed-off-by: Thomas Woerner <[email protected]>
---
 ipatests/pytest_ipa/integration/tasks.py | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
index 29774ca345..81680de1fe 100644
--- a/ipatests/pytest_ipa/integration/tasks.py
+++ b/ipatests/pytest_ipa/integration/tasks.py
@@ -53,6 +53,7 @@
 from ipatests.create_external_ca import ExternalCA
 from .env_config import env_to_script
 from .host import Host
+from .firewall import Firewall
 
 logger = logging.getLogger(__name__)
 
@@ -299,6 +300,8 @@ def install_master(host, setup_dns=True, setup_kra=False, setup_adtrust=False,
     setup_server_logs_collecting(host)
     apply_common_fixes(host)
     fix_apache_semaphores(host)
+    fw = Firewall(host)
+    fw_services = ["freeipa-ldap", "freeipa-ldaps"]
 
     args = [
         'ipa-server-install',
@@ -317,10 +320,12 @@ def install_master(host, setup_dns=True, setup_kra=False, setup_adtrust=False,
             '--forwarder', host.config.dns_forwarder,
             '--auto-reverse'
         ])
+        fw_services.append("dns")
     if setup_kra:
         args.append('--setup-kra')
     if setup_adtrust:
         args.append('--setup-adtrust')
+        fw_services.append("freeipa-trust")
     if external_ca:
         args.append('--external-ca')
 
@@ -336,6 +341,7 @@ def install_master(host, setup_dns=True, setup_kra=False, setup_adtrust=False,
             # fixup DNS zone default TTL for IPA DNS zone
             # For tests we should not wait too long
             set_default_ttl_for_ipa_dns_zone(host, raiseonerr=raiseonerr)
+        fw.enable_services(fw_services)
     return result
 
 
@@ -399,6 +405,8 @@ def install_replica(master, replica, setup_ca=True, setup_dns=False,
     apply_common_fixes(replica)
     setup_server_logs_collecting(replica)
     allow_sync_ptr(master)
+    fw = Firewall(replica)
+    fw_services = ["freeipa-ldap", "freeipa-ldaps", "freeipa-replication"]
     # Otherwise ipa-client-install would not create a PTR
     # and replica installation would fail
     args = ['ipa-replica-install',
@@ -416,8 +424,10 @@ def install_replica(master, replica, setup_ca=True, setup_dns=False,
             '--setup-dns',
             '--forwarder', replica.config.dns_forwarder
         ])
+        fw_services.append("dns")
     if setup_adtrust:
         args.append('--setup-adtrust')
+        fw_services.append("freeipa-trust")
     if master_authoritative_for_client_domain(master, replica):
         args.extend(['--ip-address', replica.ip])
 
@@ -427,6 +437,7 @@ def install_replica(master, replica, setup_ca=True, setup_dns=False,
     install_client(master, replica)
     fix_apache_semaphores(replica)
     args.extend(['-r', replica.domain.realm])
+    fw.enable_services(fw_services)
 
     result = replica.run_command(args, raiseonerr=raiseonerr,
                                  stdin_text=stdin_text)
@@ -434,6 +445,8 @@ def install_replica(master, replica, setup_ca=True, setup_dns=False,
         enable_replication_debugging(replica)
         setup_sssd_debugging(replica)
         kinit_admin(replica)
+    else:
+        fw.disable_services(fw_services)
     return result
 
 
@@ -482,6 +495,8 @@ def install_adtrust(host):
                       '-a', host.config.admin_password,
                       '--add-sids'])
 
+    Firewall(host).enable_service("freeipa-trust")
+
     # Restart named because it lost connection to dirsrv
     # (Directory server restarts during the ipa-adtrust-install)
     # we use two services named and named-pkcs11,
@@ -747,6 +762,9 @@ def uninstall_master(host, ignore_topology_disconnect=True,
 
     result = host.run_command(uninstall_cmd)
     assert "Traceback" not in result.stdout_text
+    Firewall(host).disable_services(["freeipa-ldap", "freeipa-ldaps",
+                                     "freeipa-trust", "freeipa-replication",
+                                     "dns"])
 
     host.run_command(['pkidestroy', '-s', 'CA', '-i', 'pki-tomcat'],
                      raiseonerr=False)

From fbcb58ae88839378fa84347e72909ffc895d1bd1 Mon Sep 17 00:00:00 2001
From: Thomas Woerner <[email protected]>
Date: Fri, 9 Nov 2018 11:16:23 +0100
Subject: [PATCH 3/4] 
 ipatests/test_integration/test_forced_client_reenrollment.py: Use unshare

Instead of using iptables command, use "unshare -n ip" for uninstalling
client in the restore_client method.

The uninstall_client method has been extended with the additional argument
unshare (bool) which defaults to False. With unshare set, the call for
"ipa-client-install --uninstall -U" will be used with "unshare -n ip". The
uninstall command will not have network access.

See: https://pagure.io/freeipa/issue/7755
Signed-off-by: Thomas Woerner <[email protected]>
---
 .../test_forced_client_reenrollment.py        | 45 ++++++++++---------
 1 file changed, 24 insertions(+), 21 deletions(-)

diff --git a/ipatests/test_integration/test_forced_client_reenrollment.py b/ipatests/test_integration/test_forced_client_reenrollment.py
index 8822316a17..4bb3a06275 100644
--- a/ipatests/test_integration/test_forced_client_reenrollment.py
+++ b/ipatests/test_integration/test_forced_client_reenrollment.py
@@ -173,33 +173,36 @@ def test_try_to_reenroll_with_empty_keytab(self, client):
         self.clients[0].run_command(['touch', CLIENT_KEYTAB])
         self.reenroll_client(force_join=True)
 
-    def uninstall_client(self):
+    def uninstall_client(self, unshare=False):
+        """Uninstall client
+        Set unshare to unshare the network namespace. This means that the
+        uninstall command will not have network access.
+        """
+        args = []
+        if unshare:
+            args = ['unshare', '-n', 'ip']
+        args.extend(['ipa-client-install', '--uninstall', '-U'])
         self.clients[0].run_command(
-            ['ipa-client-install', '--uninstall', '-U'],
+            args,
             set_env=False,
             raiseonerr=False
         )
 
     def restore_client(self):
-        client = self.clients[0]
-
-        client.run_command([
-            'iptables',
-            '-A', 'INPUT',
-            '-j', 'ACCEPT',
-            '-p', 'tcp',
-            '--dport', '22'
-        ])
-        for host in [self.master] + self.replicas:
-            client.run_command([
-                'iptables',
-                '-A', 'INPUT',
-                '-j', 'REJECT',
-                '-p', 'all',
-                '--source', host.ip
-            ])
-        self.uninstall_client()
-        client.run_command(['iptables', '-F'])
+        # As machine-level backup and restore is difficult to automate for
+        # testing purposes, restoring the client from backup can be simulated
+        # by performing the following step:
+        #   unshare -n ip ipa-client-install --uninstall -U
+        # Or the following steps:
+        #   iptables -A INPUT -j REJECT -p all --source $MASTER_IP
+        #   ipa-client-install --uninstall -U
+        #   iptables -F
+        # The steps described above will sever communication between server
+        # and client, and then uninstall the client. As a consequence, the
+        # client's host entry will remain on the server, ensuring that the
+        # forced re-enrollment feature works.
+
+        self.uninstall_client(unshare=True)
 
     def reenroll_client(self, keytab=None, to_replica=False, force_join=False,
                         expect_fail=False):

From 268b6dbedc8a7030e1939b5c12a3c544381335e0 Mon Sep 17 00:00:00 2001
From: Thomas Woerner <[email protected]>
Date: Fri, 9 Nov 2018 11:24:59 +0100
Subject: [PATCH 4/4] ipatests/test_integration/test_http_kdc_proxy.py: Use new
 firewall import

Instead of using ip[6]tables commands, use new firewall class to deny
access to TCP and UDP port 88 on external machines using the OUTPUT chain.
The iptables calls in the install method are replaced by a
prepend_passthrough_rules call with the rules defined in the class.

The firewall rules are defined in the class as fw_rules without
--append/-A, --delete/-D, .. First entry of each rule is the chain name,
the argument to add or delete the rule will be added by the used Firewall
method. See firewall.py for more information.

The "iptables -F" call (IPv4 only) in the uninstall method is replaced by
a remove_passthrough_rules call with the rules defined in the class.

See: https://pagure.io/freeipa/issue/7755
Signed-off-by: Thomas Woerner <[email protected]>
---
 .../test_integration/test_http_kdc_proxy.py   | 22 ++++++++-----------
 1 file changed, 9 insertions(+), 13 deletions(-)

diff --git a/ipatests/test_integration/test_http_kdc_proxy.py b/ipatests/test_integration/test_http_kdc_proxy.py
index 1c8a2dad23..aeb19d6fa9 100644
--- a/ipatests/test_integration/test_http_kdc_proxy.py
+++ b/ipatests/test_integration/test_http_kdc_proxy.py
@@ -6,6 +6,7 @@
 
 import six
 from ipatests.pytest_ipa.integration import tasks
+from ipatests.pytest_ipa.integration.firewall import Firewall
 from ipatests.test_integration.base import IntegrationTest
 from ipaplatform.paths import paths
 
@@ -17,23 +18,18 @@
 class TestHttpKdcProxy(IntegrationTest):
     topology = "line"
     num_clients = 1
+    # Firewall rules without --append/-A, --delete/-D, .. First entry of
+    # each rule is the chain name, the argument to add or delete the rule
+    # will be added by the used Firewall method. See firewall.py for more
+    # information.
+    fw_rules = [['OUTPUT', '-p', 'tcp', '--dport', '88', '-j', 'DROP'],
+                ['OUTPUT', '-p', 'udp', '--dport', '88', '-j', 'DROP']]
 
     @classmethod
     def install(cls, mh):
         super(TestHttpKdcProxy, cls).install(mh)
         # Block access from client to master's port 88
-        cls.clients[0].run_command([
-            'iptables', '-A', 'OUTPUT', '-p', 'tcp',
-            '--dport', '88', '-j', 'DROP'])
-        cls.clients[0].run_command([
-            'iptables', '-A', 'OUTPUT', '-p', 'udp',
-            '--dport', '88', '-j', 'DROP'])
-        cls.clients[0].run_command([
-            'ip6tables', '-A', 'OUTPUT', '-p', 'tcp',
-            '--dport', '88', '-j', 'DROP'])
-        cls.clients[0].run_command([
-            'ip6tables', '-A', 'OUTPUT', '-p', 'udp',
-            '--dport', '88', '-j', 'DROP'])
+        Firewall(cls.clients[0]).prepend_passthrough_rules(cls.fw_rules)
         # configure client
         cls.clients[0].run_command(
             r"sed -i 's/ kdc = .*$/ kdc = https:\/\/%s\/KdcProxy/' %s" % (
@@ -51,7 +47,7 @@ def install(cls, mh):
     @classmethod
     def uninstall(cls, mh):
         super(TestHttpKdcProxy, cls).uninstall(mh)
-        cls.clients[0].run_command(['iptables', '-F'])
+        Firewall(cls.clients[0]).remove_passthrough_rules(cls.fw_rules)
 
     def test_http_kdc_proxy_works(self):
         result = tasks.kinit_admin(self.clients[0], raiseonerr=False)
_______________________________________________
FreeIPA-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]
Fedora Code of Conduct: https://getfedora.org/code-of-conduct.html
List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines
List Archives: 
https://lists.fedorahosted.org/archives/list/[email protected]

Reply via email to