diff --git a/.gitignore b/.gitignore
index df10876..1aa675e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-SOURCES/freeipa-4.7.1.tar.gz
+SOURCES/freeipa-4.8.0.tar.gz
diff --git a/.ipa.metadata b/.ipa.metadata
index 1251b08..965cc09 100644
--- a/.ipa.metadata
+++ b/.ipa.metadata
@@ -1 +1 @@
-7c147ac996f43e83901de707958f72f795b2ce30 SOURCES/freeipa-4.7.1.tar.gz
+4cbc1d041eef6d9f5381bdfcfebf9a96d40b94cb SOURCES/freeipa-4.8.0.tar.gz
diff --git a/README.debrand b/README.debrand
deleted file mode 100644
index 01c46d2..0000000
--- a/README.debrand
+++ /dev/null
@@ -1,2 +0,0 @@
-Warning: This package was configured for automatic debranding, but the changes
-failed to apply.
diff --git a/SOURCES/0002-Fix-test_webui.test_selinuxusermap.patch b/SOURCES/0002-Fix-test_webui.test_selinuxusermap.patch
new file mode 100644
index 0000000..ab3d133
--- /dev/null
+++ b/SOURCES/0002-Fix-test_webui.test_selinuxusermap.patch
@@ -0,0 +1,124 @@
+From 96af5394c210e637a5ab81d6925be3b0a429fc08 Mon Sep 17 00:00:00 2001
+From: Stanislav Levin <slev@altlinux.org>
+Date: Fri, 5 Jul 2019 14:39:17 +0300
+Subject: [PATCH] Fix `test_webui.test_selinuxusermap`
+
+A previous refactoring of SELinux tests has have a wrong
+assumption about the user field separator within
+ipaSELinuxUserMapOrder. That was '$$', but should be just '$'.
+
+Actually, '.ldif' and '.update' files are passed through
+Python template string substitution:
+
+> $$ is an escape; it is replaced with a single $.
+> $identifier names a substitution placeholder matching
+> a mapping key of "identifier"
+
+This means that the text to be substituted on should not be escaped.
+The wrong ipaSELinuxUserMapOrder previously set will be replaced on
+upgrade.
+
+Fixes: https://pagure.io/freeipa/issue/7996
+Fixes: https://pagure.io/freeipa/issue/8005
+Signed-off-by: Stanislav Levin <slev@altlinux.org>
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ install/updates/50-ipaconfig.update                |  1 +
+ ipaplatform/base/constants.py                      | 10 +++++-----
+ ipaserver/install/ldapupdate.py                    |  3 +++
+ ipatests/test_integration/test_winsyncmigrate.py   |  2 +-
+ ipatests/test_webui/data_selinuxusermap.py         |  4 ++--
+ ipatests/test_xmlrpc/test_selinuxusermap_plugin.py |  4 ++--
+ 6 files changed, 14 insertions(+), 10 deletions(-)
+
+diff --git a/install/updates/50-ipaconfig.update b/install/updates/50-ipaconfig.update
+index 2e1c5c357..35e154b4e 100644
+--- a/install/updates/50-ipaconfig.update
++++ b/install/updates/50-ipaconfig.update
+@@ -1,4 +1,5 @@
+ dn: cn=ipaConfig,cn=etc,$SUFFIX
++replace: ipaSELinuxUserMapOrder: guest_u:s0$$$$xguest_u:s0$$$$user_u:s0$$$$staff_u:s0-s0:c0.c1023$$$$sysadm_u:s0-s0:c0.c1023$$$$unconfined_u:s0-s0:c0.c1023::$SELINUX_USERMAP_ORDER
+ replace: ipaSELinuxUserMapOrder: ipaSELinuxUserMapOrder: guest_u:s0$$xguest_u:s0$$user_u:s0$$staff_u:s0-s0:c0.c1023$$unconfined_u:s0-s0:c0.c1023::guest_u:s0$$xguest_u:s0$$user_u:s0$$staff_u:s0-s0:c0.c1023$$unconfined_u:s0-s0:c0.c1023
+ replace: ipaSELinuxUserMapOrder: guest_u:s0$$xguest_u:s0$$user_u:s0-s0:c0.c1023$$staff_u:s0-s0:c0.c1023$$unconfined_u:s0-s0:c0.c1023::guest_u:s0$$xguest_u:s0$$user_u:s0$$staff_u:s0-s0:c0.c1023$$unconfined_u:s0-s0:c0.c1023
+ add:ipaSELinuxUserMapDefault: $SELINUX_USERMAP_DEFAULT
+diff --git a/ipaplatform/base/constants.py b/ipaplatform/base/constants.py
+index cdb72e74a..eac60cac3 100644
+--- a/ipaplatform/base/constants.py
++++ b/ipaplatform/base/constants.py
+@@ -62,11 +62,11 @@ class BaseConstantsNamespace:
+     SELINUX_USERMAP_DEFAULT = "unconfined_u:s0-s0:c0.c1023"
+     SELINUX_USERMAP_ORDER = (
+         "guest_u:s0"
+-        "$$xguest_u:s0"
+-        "$$user_u:s0"
+-        "$$staff_u:s0-s0:c0.c1023"
+-        "$$sysadm_u:s0-s0:c0.c1023"
+-        "$$unconfined_u:s0-s0:c0.c1023"
++        "$xguest_u:s0"
++        "$user_u:s0"
++        "$staff_u:s0-s0:c0.c1023"
++        "$sysadm_u:s0-s0:c0.c1023"
++        "$unconfined_u:s0-s0:c0.c1023"
+     )
+     SSSD_USER = "sssd"
+     # WSGI module override, only used on Fedora
+diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py
+index d9e47dcc0..0cdea6a82 100644
+--- a/ipaserver/install/ldapupdate.py
++++ b/ipaserver/install/ldapupdate.py
+@@ -322,6 +322,9 @@ class LDAPUpdate:
+         if not self.sub_dict.get("SELINUX_USERMAP_DEFAULT"):
+             self.sub_dict["SELINUX_USERMAP_DEFAULT"] = \
+                 platformconstants.SELINUX_USERMAP_DEFAULT
++        if not self.sub_dict.get("SELINUX_USERMAP_ORDER"):
++            self.sub_dict["SELINUX_USERMAP_ORDER"] = \
++                platformconstants.SELINUX_USERMAP_ORDER
+         self.api = create_api(mode=None)
+         self.api.bootstrap(in_server=True,
+                            context='updates',
+diff --git a/ipatests/test_integration/test_winsyncmigrate.py b/ipatests/test_integration/test_winsyncmigrate.py
+index 593fc2065..be9f44072 100644
+--- a/ipatests/test_integration/test_winsyncmigrate.py
++++ b/ipatests/test_integration/test_winsyncmigrate.py
+@@ -59,7 +59,7 @@ class TestWinsyncMigrate(IntegrationTest):
+     ipa_group = 'ipa_group'
+     ad_user = 'testuser'
+     default_shell = platformconstants.DEFAULT_SHELL
+-    selinuxuser = platformconstants.SELINUX_USERMAP_ORDER.split("$$")[0]
++    selinuxuser = platformconstants.SELINUX_USERMAP_ORDER.split("$")[0]
+     test_role = 'test_role'
+     test_hbac_rule = 'test_hbac_rule'
+     test_selinux_map = 'test_selinux_map'
+diff --git a/ipatests/test_webui/data_selinuxusermap.py b/ipatests/test_webui/data_selinuxusermap.py
+index ca7b1dcdd..312e7592f 100644
+--- a/ipatests/test_webui/data_selinuxusermap.py
++++ b/ipatests/test_webui/data_selinuxusermap.py
+@@ -5,8 +5,8 @@
+ from ipaplatform.constants import constants as platformconstants
+ 
+ # for example, user_u:s0
+-selinuxuser1 = platformconstants.SELINUX_USERMAP_ORDER.split("$$")[0]
+-selinuxuser2 = platformconstants.SELINUX_USERMAP_ORDER.split("$$")[1]
++selinuxuser1 = platformconstants.SELINUX_USERMAP_ORDER.split("$")[0]
++selinuxuser2 = platformconstants.SELINUX_USERMAP_ORDER.split("$")[1]
+ 
+ selinux_mcs_max = platformconstants.SELINUX_MCS_MAX
+ selinux_mls_max = platformconstants.SELINUX_MLS_MAX
+diff --git a/ipatests/test_xmlrpc/test_selinuxusermap_plugin.py b/ipatests/test_xmlrpc/test_selinuxusermap_plugin.py
+index 0b73992aa..e5b23bd4d 100644
+--- a/ipatests/test_xmlrpc/test_selinuxusermap_plugin.py
++++ b/ipatests/test_xmlrpc/test_selinuxusermap_plugin.py
+@@ -32,8 +32,8 @@ from ipatests.test_xmlrpc.test_user_plugin import get_user_result
+ import pytest
+ 
+ rule1 = u'selinuxrule1'
+-selinuxuser1 = platformconstants.SELINUX_USERMAP_ORDER.split("$$")[0]
+-selinuxuser2 = platformconstants.SELINUX_USERMAP_ORDER.split("$$")[1]
++selinuxuser1 = platformconstants.SELINUX_USERMAP_ORDER.split("$")[0]
++selinuxuser2 = platformconstants.SELINUX_USERMAP_ORDER.split("$")[1]
+ 
+ INVALID_MCS = "Invalid MCS value, must match {}, where max category {}".format(
+     platformconstants.SELINUX_MCS_REGEX,
+-- 
+2.21.0
+
diff --git a/SOURCES/0002-freeipa-4.7.0-ipaclient-Remove-no-sssd-and-noac-options_rhbz#1614301.patch b/SOURCES/0002-freeipa-4.7.0-ipaclient-Remove-no-sssd-and-noac-options_rhbz#1614301.patch
deleted file mode 100644
index 7447a08..0000000
--- a/SOURCES/0002-freeipa-4.7.0-ipaclient-Remove-no-sssd-and-noac-options_rhbz#1614301.patch
+++ /dev/null
@@ -1,77 +0,0 @@
-From c5cdd5a5f01306b3a70354d34079efe64565aa69 Mon Sep 17 00:00:00 2001
-From: Thomas Woerner <twoerner@redhat.com>
-Date: Thu, 9 Aug 2018 12:05:26 +0200
-Subject: ipaclient: Remove --no-sssd and --no-ac options
-
-Client installation with --no-sssd option has already beeen deprecated
-with https://pagure.io/freeipa/issue/5860. Authconfig support has been
-removed, therefore --no-ac option can be removed also.
-
-ipatests/test_integration/test_authselect.py: Skip no_sssd and no_ac tests.
-
-See: https://pagure.io/freeipa/issue/7671
-Signed-off-by: Thomas Woerner <twoerner@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- ipaclient/install/client.py                  | 6 +-----
- ipaclient/install/sssd.py                    | 9 +--------
- ipatests/test_integration/test_authselect.py | 2 ++
- 3 files changed, 4 insertions(+), 13 deletions(-)
-
-diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
-index 627351ac2..800a46734 100644
---- a/ipaclient/install/client.py
-+++ b/ipaclient/install/client.py
-@@ -3709,11 +3709,7 @@ class ClientInstall(ClientInstallInterface,
-     def prompt_password(self):
-         return self.interactive
- 
--    no_ac = knob(
--        None,
--        description="do not modify the nsswitch.conf and PAM configuration",
--        cli_names='--noac',
--    )
-+    no_ac = False
- 
-     force = knob(
-         None,
-diff --git a/ipaclient/install/sssd.py b/ipaclient/install/sssd.py
-index b20abde56..98b850464 100644
---- a/ipaclient/install/sssd.py
-+++ b/ipaclient/install/sssd.py
-@@ -43,11 +43,4 @@ class SSSDInstallInterface(service.ServiceInstallInterface):
-     )
-     preserve_sssd = enroll_only(preserve_sssd)
- 
--    no_sssd = knob(
--        None,
--        deprecated=True,
--        description="Do not configure the client to use SSSD for "
--                    "authentication",
--        cli_names=[None, '-S'],
--    )
--    no_sssd = enroll_only(no_sssd)
-+    no_sssd = False
-diff --git a/ipatests/test_integration/test_authselect.py b/ipatests/test_integration/test_authselect.py
-index fa9b20265..ebf3d9892 100644
---- a/ipatests/test_integration/test_authselect.py
-+++ b/ipatests/test_integration/test_authselect.py
-@@ -88,6 +88,7 @@ class TestClientInstallation(IntegrationTest):
-             ['ipa-client-install', '--uninstall', '-U'],
-             raiseonerr=False)
- 
-+    @pytest.mark.skip(reason="Option --no-sssd has been removed")
-     def test_install_client_no_sssd(self):
-         """
-         Test client installation with --no-sssd option.
-@@ -98,6 +99,7 @@ class TestClientInstallation(IntegrationTest):
-         msg = "Option '--no-sssd' is incompatible with the 'authselect' tool"
-         assert msg in result.stderr_text
- 
-+    @pytest.mark.skip(reason="Option --noac has been removed")
-     def test_install_client_no_ac(self):
-         """
-         Test client installation with --noac option.
--- 
-2.17.1
-
diff --git a/SOURCES/0003-Remove-posixAccount-from-service_find-search-filter-2f9cbff_rhbz#1731437.patch b/SOURCES/0003-Remove-posixAccount-from-service_find-search-filter-2f9cbff_rhbz#1731437.patch
new file mode 100644
index 0000000..17c769a
--- /dev/null
+++ b/SOURCES/0003-Remove-posixAccount-from-service_find-search-filter-2f9cbff_rhbz#1731437.patch
@@ -0,0 +1,33 @@
+From 2f9cbffb6e57ded2d0107f457241f33b17869a96 Mon Sep 17 00:00:00 2001
+From: Rob Crittenden <rcritten@redhat.com>
+Date: Jul 19 2019 19:16:16 +0000
+Subject: Remove posixAccount from service_find search filter
+
+
+This will allow cifs principals to be found. They were suppressed
+because they include objectclass=posixAccount.
+
+This is a bit of a historical anomaly. This was included in the
+filter from the initial commit (though it was person, not
+posixAccount). I believe it was a mistake from the beginning but
+it wasn't noticed because it didn't cause any obvious issues.
+
+https://pagure.io/freeipa/issue/8013
+
+Reviewed-By: Alexander Bokovoy <abbra@users.noreply.github.com>
+
+---
+
+diff --git a/ipaserver/plugins/service.py b/ipaserver/plugins/service.py
+index f58fe4b..c118b80 100644
+--- a/ipaserver/plugins/service.py
++++ b/ipaserver/plugins/service.py
+@@ -889,7 +889,6 @@ class service_find(LDAPSearch):
+         assert isinstance(base_dn, DN)
+         # lisp style!
+         custom_filter = '(&(objectclass=ipaService)' \
+-                          '(!(objectClass=posixAccount))' \
+                           '(!(|(krbprincipalname=kadmin/*)' \
+                               '(krbprincipalname=K/M@*)' \
+                               '(krbprincipalname=krbtgt/*))' \
+
diff --git a/SOURCES/0003-adtrust-define-Guests-mapping.patch b/SOURCES/0003-adtrust-define-Guests-mapping.patch
deleted file mode 100644
index ab55aeb..0000000
--- a/SOURCES/0003-adtrust-define-Guests-mapping.patch
+++ /dev/null
@@ -1,50 +0,0 @@
-From 1ef0fe8bb824282c2f48417efda3a60e7c1bf580 Mon Sep 17 00:00:00 2001
-From: Alexander Bokovoy <abokovoy@redhat.com>
-Date: Tue, 9 Oct 2018 17:21:37 +0300
-Subject: [PATCH] adtrust: define Guests mapping after creating cifs/ principal
-
-All Samba utilities load passdb modules from the configuration file. As
-result, 'net groupmap' call would try to initialize ipasam passdb module
-and that one would try to connect to LDAP using Kerberos authentication.
-
-We should be running it after cifs/ principal is actually created in
-ipa-adtrust-install or otherwise setting up group mapping will fail.
-
-This only affects new installations. For older ones 'net groupmap' would
-work just fine because adtrust is already configured and all principals
-exist already.
-
-A re-run of 'ipa-server-upgrade' is a workaround too but better to fix
-the initial setup.
-
-Related: https://pagure.io/freeipa/issue/7705
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipaserver/install/adtrustinstance.py | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/ipaserver/install/adtrustinstance.py b/ipaserver/install/adtrustinstance.py
-index 46c4ad663..da16748cf 100644
---- a/ipaserver/install/adtrustinstance.py
-+++ b/ipaserver/install/adtrustinstance.py
-@@ -837,8 +837,6 @@ class ADTRUSTInstance(service.Service):
-                   self.__create_samba_domain_object)
-         self.step("creating samba config registry", self.__write_smb_registry)
-         self.step("writing samba config file", self.__write_smb_conf)
--        self.step("map BUILTIN\\Guests to nobody group",
--                  self.__map_Guests_to_nobody)
-         self.step("adding cifs Kerberos principal",
-                   self.request_service_keytab)
-         self.step("adding cifs and host Kerberos principals to the adtrust agents group", \
-@@ -850,6 +848,8 @@ class ADTRUSTInstance(service.Service):
-         self.step("updating Kerberos config", self.__update_krb5_conf)
-         self.step("activating CLDAP plugin", self.__add_cldap_module)
-         self.step("activating sidgen task", self.__add_sidgen_task)
-+        self.step("map BUILTIN\\Guests to nobody group",
-+                  self.__map_Guests_to_nobody)
-         self.step("configuring smbd to start on boot", self.__enable)
-         self.step("adding special DNS service records", \
-                   self.__add_dns_service_records)
--- 
-2.17.1
-
diff --git a/SOURCES/0004-Repeated-uninstallation-of-ipa-client-samba-crashes_rhbz#1732529.patch b/SOURCES/0004-Repeated-uninstallation-of-ipa-client-samba-crashes_rhbz#1732529.patch
new file mode 100644
index 0000000..374534b
--- /dev/null
+++ b/SOURCES/0004-Repeated-uninstallation-of-ipa-client-samba-crashes_rhbz#1732529.patch
@@ -0,0 +1,146 @@
+From b9b98097a47f27b56500edc972c438597e6609b1 Mon Sep 17 00:00:00 2001
+From: François Cami <fcami@redhat.com>
+Date: Jul 26 2019 13:09:42 +0000
+Subject: ipatests: test multiple invocations of ipa-client-samba --uninstall
+
+
+Related-to: https://pagure.io/freeipa/issue/8019
+Signed-off-by: François Cami <fcami@redhat.com>
+Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+Reviewed-By: Sergey Orlov <sorlov@redhat.com>
+
+---
+
+diff --git a/ipatests/test_integration/test_smb.py b/ipatests/test_integration/test_smb.py
+index 4e295c0..26d70b3 100644
+--- a/ipatests/test_integration/test_smb.py
++++ b/ipatests/test_integration/test_smb.py
+@@ -150,3 +150,6 @@ class TestSMB(IntegrationTest):
+ 
+         smbsrv = self.replicas[0]
+         smbsrv.run_command(['ipa-client-samba', '--uninstall', '-U'])
++        # test for https://pagure.io/freeipa/issue/8019
++        # try another uninstall after the first one:
++        smbsrv.run_command(['ipa-client-samba', '--uninstall', '-U'])
+
+From 256a6a879061d2b97c11e9cd97b2427579610fa1 Mon Sep 17 00:00:00 2001
+From: François Cami <fcami@redhat.com>
+Date: Jul 26 2019 13:09:42 +0000
+Subject: ipa-client-samba: remove and restore smb.conf only on first uninstall
+
+
+Fixes: https://pagure.io/freeipa/issue/8019
+Signed-off-by: François Cami <fcami@redhat.com>
+Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+Reviewed-By: Sergey Orlov <sorlov@redhat.com>
+
+---
+
+diff --git a/ipaclient/install/ipa_client_samba.py b/ipaclient/install/ipa_client_samba.py
+index e2be67d..6a3c3bd 100755
+--- a/ipaclient/install/ipa_client_samba.py
++++ b/ipaclient/install/ipa_client_samba.py
+@@ -433,8 +433,9 @@ def uninstall(fstore, statestore, options):
+     ipautil.remove_ccache(ccache_path=paths.KRB5CC_SAMBA)
+ 
+     # Remove samba's configuration file
+-    ipautil.remove_file(paths.SMB_CONF)
+-    fstore.restore_file(paths.SMB_CONF)
++    if fstore.has_file(paths.SMB_CONF):
++        ipautil.remove_file(paths.SMB_CONF)
++        fstore.restore_file(paths.SMB_CONF)
+ 
+     # Remove samba's persistent and temporary tdb files
+     tdb_files = [
+@@ -624,7 +625,7 @@ def run():
+             api.Command.service_del(api.env.smb_princ)
+         except AttributeError:
+             logger.error(
+-                "Chosen IPA master %s does not have support to"
++                "Chosen IPA master %s does not have support to "
+                 "set up Samba domain members", server,
+             )
+             return 1
+
+From 00ba2ae6681dafa92d3f00f2a4e11adaa477ea0e Mon Sep 17 00:00:00 2001
+From: François Cami <fcami@redhat.com>
+Date: Jul 26 2019 13:09:42 +0000
+Subject: ipatests: test ipa-client-samba after --uninstall
+
+
+Related-to: https://pagure.io/freeipa/issue/8021
+Signed-off-by: François Cami <fcami@redhat.com>
+Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+Reviewed-By: Sergey Orlov <sorlov@redhat.com>
+
+---
+
+diff --git a/ipatests/test_integration/test_smb.py b/ipatests/test_integration/test_smb.py
+index 26d70b3..933d468 100644
+--- a/ipatests/test_integration/test_smb.py
++++ b/ipatests/test_integration/test_smb.py
+@@ -153,3 +153,8 @@ class TestSMB(IntegrationTest):
+         # test for https://pagure.io/freeipa/issue/8019
+         # try another uninstall after the first one:
+         smbsrv.run_command(['ipa-client-samba', '--uninstall', '-U'])
++        # test for https://pagure.io/freeipa/issue/8021
++        # try to install again:
++        smbsrv.run_command(["ipa-client-samba", "-U"])
++        # cleanup:
++        smbsrv.run_command(['ipa-client-samba', '--uninstall', '-U'])
+
+From 551cd68d0959b1ee761ead6338dc06c544c0c5da Mon Sep 17 00:00:00 2001
+From: François Cami <fcami@redhat.com>
+Date: Jul 26 2019 13:09:42 +0000
+Subject: ipa-client-samba: remove state on uninstall
+
+
+The "domain_member" state was not removed at uninstall time.
+Remove it so that future invocations of ipa-client-samba work.
+
+Fixes: https://pagure.io/freeipa/issue/8021
+Signed-off-by: François Cami <fcami@redhat.com>
+
+https://pagure.io/freeipa/issue/8021
+
+Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+Reviewed-By: Sergey Orlov <sorlov@redhat.com>
+
+---
+
+diff --git a/ipaclient/install/ipa_client_samba.py b/ipaclient/install/ipa_client_samba.py
+index 6a3c3bd..126ef32 100755
+--- a/ipaclient/install/ipa_client_samba.py
++++ b/ipaclient/install/ipa_client_samba.py
+@@ -523,11 +523,25 @@ def run():
+     if options.uninstall:
+         if statestore.has_state("domain_member"):
+             uninstall(fstore, statestore, options)
+-            print(
+-                "Samba configuration is reverted. "
+-                "However, Samba databases were fully cleaned and "
+-                "old configuration file will not be usable anymore."
+-            )
++            try:
++                keys = (
++                    "configured", "hardening", "groupmap", "tdb",
++                    "service.principal", "smb.conf"
++                )
++                for key in keys:
++                    statestore.delete_state("domain_member", key)
++            except Exception as e:
++                print(
++                    "Error: Failed to remove the domain_member statestores: "
++                    "%s" % e
++                )
++                return 1
++            else:
++                print(
++                    "Samba configuration is reverted. "
++                    "However, Samba databases were fully cleaned and "
++                    "old configuration file will not be usable anymore."
++                )
+         else:
+             print("Samba domain member is not configured yet")
+         return 0
+
diff --git a/SOURCES/0004-freeipa-4.7.1-Find_orphan_automember_rules_rhbz#1638373.patch b/SOURCES/0004-freeipa-4.7.1-Find_orphan_automember_rules_rhbz#1638373.patch
deleted file mode 100644
index d650aef..0000000
--- a/SOURCES/0004-freeipa-4.7.1-Find_orphan_automember_rules_rhbz#1638373.patch
+++ /dev/null
@@ -1,209 +0,0 @@
-From 67875c3b75ad1af493ff5930f9c5fd5e9797b775 Mon Sep 17 00:00:00 2001
-From: Thomas Woerner <twoerner@redhat.com>
-Date: Oct 12 2018 07:50:29 +0000
-Subject: Find orphan automember rules
-
-
-If groups or hostgroups have been removed after automember rules have been
-created using them, then automember-rebuild, automember-add, host-add and
-more commands could fail.
-
-A new command has been added to the ipa tool:
-
-  ipa automember-find-orphans --type={hostgroup,group} [--remove]
-
-This command retuns the list of orphan automember rules in the same way as
-automember-find. With the --remove option the orphan rules are also removed.
-
-The IPA API version has been increased and a test case has been added.
-
-Using ideas from a patch by: Rob Crittenden <rcritten@redhat.com>
-
-See: https://pagure.io/freeipa/issue/6476
-Signed-off-by: Thomas Woerner <twoerner@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
-Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
-
----
-
-diff --git a/API.txt b/API.txt
-index 49216cb..93e1a38 100644
---- a/API.txt
-+++ b/API.txt
-@@ -186,6 +186,20 @@ output: Output('count', type=[<type 'int'>])
- output: ListOfEntries('result')
- output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
- output: Output('truncated', type=[<type 'bool'>])
-+command: automember_find_orphans/1
-+args: 1,7,4
-+arg: Str('criteria?')
-+option: Flag('all', autofill=True, cli_name='all', default=False)
-+option: Str('description?', autofill=False, cli_name='desc')
-+option: Flag('pkey_only?', autofill=True, default=False)
-+option: Flag('raw', autofill=True, cli_name='raw', default=False)
-+option: Flag('remove?', autofill=True, default=False)
-+option: StrEnum('type', values=[u'group', u'hostgroup'])
-+option: Str('version?')
-+output: Output('count', type=[<type 'int'>])
-+output: ListOfEntries('result')
-+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
-+output: Output('truncated', type=[<type 'bool'>])
- command: automember_mod/1
- args: 1,9,3
- arg: Str('cn', cli_name='automember_rule')
-@@ -6503,6 +6517,7 @@ default: automember_default_group_set/1
- default: automember_default_group_show/1
- default: automember_del/1
- default: automember_find/1
-+default: automember_find_orphans/1
- default: automember_mod/1
- default: automember_rebuild/1
- default: automember_remove_condition/1
-diff --git a/VERSION.m4 b/VERSION.m4
-index f437ef0..9d5532c 100644
---- a/VERSION.m4
-+++ b/VERSION.m4
-@@ -83,8 +83,8 @@ define(IPA_DATA_VERSION, 20100614120000)
- #                                                      #
- ########################################################
- define(IPA_API_VERSION_MAJOR, 2)
--define(IPA_API_VERSION_MINOR, 229)
--# Last change: Added the Certificate parameter
-+define(IPA_API_VERSION_MINOR, 230)
-+# Last change: Added `automember-find-orphans' command
- 
- 
- ########################################################
-diff --git a/ipaserver/plugins/automember.py b/ipaserver/plugins/automember.py
-index a502aea..a7f468d 100644
---- a/ipaserver/plugins/automember.py
-+++ b/ipaserver/plugins/automember.py
-@@ -117,6 +117,11 @@ EXAMPLES:
-  Find all of the automember rules:
-     ipa automember-find
- """) + _("""
-+ Find all of the orphan automember rules:
-+    ipa automember-find-orphans --type=hostgroup
-+ Find all of the orphan automember rules and remove them:
-+    ipa automember-find-orphans --type=hostgroup --remove
-+""") + _("""
-  Display a automember rule:
-     ipa automember-show --type=hostgroup webservers
-     ipa automember-show --type=group devel
-@@ -817,3 +822,58 @@ class automember_rebuild(Method):
-             result=result,
-             summary=unicode(summary),
-             value=pkey_to_value(None, options))
-+
-+
-+@register()
-+class automember_find_orphans(LDAPSearch):
-+    __doc__ = _("""
-+    Search for orphan automember rules. The command might need to be run as
-+    a privileged user user to get all orphan rules.
-+    """)
-+    takes_options = group_type + (
-+        Flag(
-+            'remove?',
-+            doc=_("Remove orphan automember rules"),
-+        ),
-+    )
-+
-+    msg_summary = ngettext(
-+        '%(count)d rules matched', '%(count)d rules matched', 0
-+    )
-+
-+    def execute(self, *keys, **options):
-+        results = super(automember_find_orphans, self).execute(*keys,
-+                                                               **options)
-+
-+        remove_option = options.get('remove')
-+        pkey_only = options.get('pkey_only', False)
-+        ldap = self.obj.backend
-+        orphans = []
-+        for entry in results["result"]:
-+            am_dn_entry = entry['automembertargetgroup'][0]
-+            # Make DN for --raw option
-+            if not isinstance(am_dn_entry, DN):
-+                am_dn_entry = DN(am_dn_entry)
-+            try:
-+                ldap.get_entry(am_dn_entry)
-+            except errors.NotFound:
-+                if pkey_only:
-+                    # For pkey_only remove automembertargetgroup
-+                    del(entry['automembertargetgroup'])
-+                orphans.append(entry)
-+                if remove_option:
-+                    ldap.delete_entry(entry['dn'])
-+
-+        results["result"][:] = orphans
-+        results["count"] = len(orphans)
-+        return results
-+
-+    def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args,
-+                     **options):
-+        assert isinstance(base_dn, DN)
-+        scope = ldap.SCOPE_SUBTREE
-+        ndn = DN(('cn', options['type']), base_dn)
-+        if options.get('pkey_only', False):
-+            # For pkey_only add automembertargetgroup
-+            attrs_list.append('automembertargetgroup')
-+        return filters, ndn, scope
-diff --git a/ipatests/test_xmlrpc/test_automember_plugin.py b/ipatests/test_xmlrpc/test_automember_plugin.py
-index ffbc911..c83e11a 100644
---- a/ipatests/test_xmlrpc/test_automember_plugin.py
-+++ b/ipatests/test_xmlrpc/test_automember_plugin.py
-@@ -715,3 +715,51 @@ class TestMultipleAutomemberConditions(XMLRPC_test):
- 
-         defaultgroup1.ensure_missing()
-         defaulthostgroup1.ensure_missing()
-+
-+
-+@pytest.mark.tier1
-+class TestAutomemberFindOrphans(XMLRPC_test):
-+    def test_create_deps_for_find_orphans(self, hostgroup1, host1,
-+                                          automember_hostgroup):
-+        """ Create host, hostgroup, and automember tracker for this class
-+        of tests. """
-+
-+        # Create hostgroup1 and automember rule with condition
-+        hostgroup1.ensure_exists()
-+        host1.ensure_exists()
-+
-+        # Manually create automember rule and condition, racker will try to
-+        # remove the automember rule in the end, which is failing as the rule
-+        # is already removed
-+        api.Command['automember_add'](hostgroup1.cn, type=u'hostgroup')
-+        api.Command['automember_add_condition'](
-+            hostgroup1.cn,
-+            key=u'fqdn', type=u'hostgroup',
-+            automemberinclusiveregex=[hostgroup_include_regex]
-+        )
-+
-+        hostgroup1.retrieve()
-+
-+    def test_find_orphan_automember_rules(self, hostgroup1):
-+        """ Remove hostgroup1, find and remove obsolete automember rules. """
-+        # Remove hostgroup1
-+
-+        hostgroup1.ensure_missing()
-+
-+        # Find obsolete automember rules
-+        result = api.Command['automember_find_orphans'](type=u'hostgroup')
-+        assert result['count'] == 1
-+
-+        # Find and remove obsolete automember rules
-+        result = api.Command['automember_find_orphans'](type=u'hostgroup',
-+                                                        remove=True)
-+        assert result['count'] == 1
-+
-+        # Find obsolete automember rules
-+        result = api.Command['automember_find_orphans'](type=u'hostgroup')
-+        assert result['count'] == 0
-+
-+        # Final cleanup of automember rule if it still exists
-+        with raises_exact(errors.NotFound(
-+                reason=u'%s: Automember rule not found' % hostgroup1.cn)):
-+            api.Command['automember_del'](hostgroup1.cn, type=u'hostgroup')
-
diff --git a/SOURCES/0005-WebUI-Add-PKINIT-status-field-to-Configuration-page-a46383f_rhbz#1518153.patch b/SOURCES/0005-WebUI-Add-PKINIT-status-field-to-Configuration-page-a46383f_rhbz#1518153.patch
new file mode 100644
index 0000000..1060da1
--- /dev/null
+++ b/SOURCES/0005-WebUI-Add-PKINIT-status-field-to-Configuration-page-a46383f_rhbz#1518153.patch
@@ -0,0 +1,72 @@
+From a46383ffe414f703264f8a81450f44abbd95d78e Mon Sep 17 00:00:00 2001
+From: Serhii Tsymbaliuk <stsymbal@redhat.com>
+Date: Jul 26 2019 20:36:58 +0000
+Subject: WebUI: Add PKINIT status field to 'Configuration' page
+
+
+- Add 'Server Options' section to the page
+- Add 'IPA master capable of PKINIT' field to the 'Server Options'
+
+Ticket: https://pagure.io/freeipa/issue/7305
+
+Signed-off-by: Serhii Tsymbaliuk <stsymbal@redhat.com>
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+
+---
+
+diff --git a/install/ui/src/freeipa/serverconfig.js b/install/ui/src/freeipa/serverconfig.js
+index 25f484a..6c82b40 100644
+--- a/install/ui/src/freeipa/serverconfig.js
++++ b/install/ui/src/freeipa/serverconfig.js
+@@ -50,6 +50,24 @@ return {
+                     ]
+                 },
+                 {
++                    name: 'server',
++                    label: '@i18n:objects.config.server',
++                    fields: [
++                        {
++                            $type: 'entity_select',
++                            name: 'ca_renewal_master_server',
++                            other_entity: 'server',
++                            other_field: 'cn',
++                            flags: ['w_if_no_aci']
++                        },
++                        {
++                            $type: 'multivalued',
++                            name: 'pkinit_server_server',
++                            read_only: true
++                        }
++                    ]
++                },
++                {
+                     name: 'user',
+                     label: '@i18n:objects.config.user',
+                     fields: [
+@@ -99,13 +117,6 @@ return {
+                         {
+                             $type: 'multivalued',
+                             name: 'ipauserobjectclasses'
+-                        },
+-                        {
+-                            $type: 'entity_select',
+-                            name: 'ca_renewal_master_server',
+-                            other_entity: 'server',
+-                            other_field: 'cn',
+-                            flags: ['w_if_no_aci']
+                         }
+                     ]
+                 },
+diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
+index 0f0ad3a..19957d7 100644
+--- a/ipaserver/plugins/internal.py
++++ b/ipaserver/plugins/internal.py
+@@ -726,6 +726,7 @@ class i18n_messages(Command):
+                 "group": _("Group Options"),
+                 "search": _("Search Options"),
+                 "selinux": _("SELinux Options"),
++                "server": _("Server Options"),
+                 "service": _("Service Options"),
+                 "user": _("User Options"),
+             },
+
diff --git a/SOURCES/0005-net-groupmap-force-using-empty-config.patch b/SOURCES/0005-net-groupmap-force-using-empty-config.patch
deleted file mode 100644
index 2d07145..0000000
--- a/SOURCES/0005-net-groupmap-force-using-empty-config.patch
+++ /dev/null
@@ -1,55 +0,0 @@
-From 7f8858f8632d77497765bab79922f1762ce46d50 Mon Sep 17 00:00:00 2001
-From: Alexander Bokovoy <abokovoy@redhat.com>
-Date: Tue, 16 Oct 2018 17:54:09 +0300
-Subject: [PATCH] net groupmap: force using empty config when mapping Guests
-
-When we define a group mapping for BUILTIN\Guests to 'nobody' group in
-we run 'net groupmap add ...' with a default /etc/samba/smb.conf which
-is now configured to use ipasam passdb module. We authenticate to LDAP
-with GSSAPI in ipasam passdb module initialization.
-
-If GSSAPI authentication failed (KDC is offline, for example, during
-server upgrade), 'net groupmap add' crashes after ~10 attempts to
-re-authenticate. This is intended behavior in smbd/winbindd as they
-cannot work anymore. However, for the command line tools there are
-plenty of operations where passdb module is not needed.
-
-Additionally, GSSAPI authentication uses the default ccache in the
-environment and a key from /etc/samba/samba.keytab keytab. This means
-that if you'd run 'net *' as root, it will replace whatever Kerberos
-tickets you have with a TGT for cifs/`hostname` and a service ticket to
-ldap/`hostname` of IPA master.
-
-Apply a simple solution to avoid using /etc/samba/smb.conf when we
-set up the group mapping by specifying '-s /dev/null' in 'net groupmap'
-call.
-
-For upgrade code this is enough as in
-a678336b8b36cdbea2512e79c09e475fdc249569 we enforce use of empty
-credentials cache during upgrade to prevent tripping on individual
-ccaches from KEYRING: or KCM: cache collections.
-
-Related: https://pagure.io/freeipa/issue/7705
-(cherry picked from commit e48f5a4d64d95c4c5cb5f8ede39cae5c7c1e512c)
----
- ipaserver/install/adtrustinstance.py | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/ipaserver/install/adtrustinstance.py b/ipaserver/install/adtrustinstance.py
-index da16748cf..3a751ccb2 100644
---- a/ipaserver/install/adtrustinstance.py
-+++ b/ipaserver/install/adtrustinstance.py
-@@ -114,8 +114,8 @@ def make_netbios_name(s):
- 
- def map_Guests_to_nobody():
-     env = {'LC_ALL': 'C'}
--    args = [paths.NET, 'groupmap', 'add', 'sid=S-1-5-32-546',
--            'unixgroup=nobody', 'type=builtin']
-+    args = [paths.NET, '-s', '/dev/null', 'groupmap', 'add',
-+            'sid=S-1-5-32-546', 'unixgroup=nobody', 'type=builtin']
- 
-     logger.debug("Map BUILTIN\\Guests to a group 'nobody'")
-     ipautil.run(args, env=env, raiseonerr=False, capture_error=True)
--- 
-2.17.2
-
diff --git a/SOURCES/0006-Keep-dogtags-client-db-in-external-ca-step-1.patch b/SOURCES/0006-Keep-dogtags-client-db-in-external-ca-step-1.patch
deleted file mode 100644
index 4394a2b..0000000
--- a/SOURCES/0006-Keep-dogtags-client-db-in-external-ca-step-1.patch
+++ /dev/null
@@ -1,126 +0,0 @@
-From 78bf80e55dd74fc0279cf6a76345865b0d5e5d32 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Fri, 26 Oct 2018 18:12:29 +0200
-Subject: [PATCH] Keep Dogtag's client db in external CA step 1
-
-Don't remove /root/.dogtag/pki-tomcat when performing step 1 of external
-CA installation process. Dogtag 10.6.7 changed behavior and no longer
-re-creates the client database in step 2.
-
-Fixes: https://pagure.io/freeipa/issue/7742
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
-
-diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
-index 59c0eadf1..61ccb6dff 100644
---- a/ipaserver/install/cainstance.py
-+++ b/ipaserver/install/cainstance.py
-@@ -483,7 +483,12 @@ class CAInstance(DogtagInstance):
-         try:
-             self.start_creation(runtime=runtime)
-         finally:
--            self.clean_pkispawn_files()
-+            if self.external == 1:
-+                # Don't remove client DB in external CA step 1
-+                # https://pagure.io/freeipa/issue/7742
-+                logger.debug("Keep pkispawn files for step 2")
-+            else:
-+                self.clean_pkispawn_files()
- 
-     def __spawn_instance(self):
-         """
-diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py
-index e71bf2900..142a8c0d7 100644
---- a/ipaserver/install/dogtaginstance.py
-+++ b/ipaserver/install/dogtaginstance.py
-@@ -167,11 +167,13 @@ class DogtagInstance(service.Service):
- 
-     def clean_pkispawn_files(self):
-         if self.tmp_agent_db is not None:
-+            logger.debug("Removing %s", self.tmp_agent_db)
-             shutil.rmtree(self.tmp_agent_db, ignore_errors=True)
- 
--        shutil.rmtree('/root/.dogtag/pki-tomcat/{subsystem}/'
--                      .format(subsystem=self.subsystem.lower()),
--                      ignore_errors=True)
-+        client_dir = os.path.join(
-+            '/root/.dogtag/pki-tomcat/', self.subsystem.lower())
-+        logger.debug("Removing %s", client_dir)
-+        shutil.rmtree(client_dir, ignore_errors=True)
- 
-     def restart_instance(self):
-         self.restart('pki-tomcat')
-
-From 6214fc51789dcfc70d4df18c0153877b92625ad2 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Fri, 26 Oct 2018 10:11:31 +0200
-Subject: [PATCH] Use tasks.install_master() in external_ca tests
-
-The install_master() function performs additional steps besides just
-installing a server. It also sets up log collection and performs
-additional tests.
-
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
-
-diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
-index 620ed28c9..9889636ba 100644
---- a/ipatests/pytest_ipa/integration/tasks.py
-+++ b/ipatests/pytest_ipa/integration/tasks.py
-@@ -292,7 +292,7 @@ def set_default_ttl_for_ipa_dns_zone(host, raiseonerr=True):
- 
- def install_master(host, setup_dns=True, setup_kra=False, setup_adtrust=False,
-                    extra_args=(), domain_level=None, unattended=True,
--                   stdin_text=None, raiseonerr=True):
-+                   external_ca=False, stdin_text=None, raiseonerr=True):
-     if domain_level is None:
-         domain_level = host.config.domain_level
-     check_domain_level(domain_level)
-@@ -321,11 +321,14 @@ def install_master(host, setup_dns=True, setup_kra=False, setup_adtrust=False,
-         args.append('--setup-kra')
-     if setup_adtrust:
-         args.append('--setup-adtrust')
-+    if external_ca:
-+        args.append('--external-ca')
- 
-     args.extend(extra_args)
-     result = host.run_command(args, raiseonerr=raiseonerr,
-                               stdin_text=stdin_text)
--    if result.returncode == 0:
-+    if result.returncode == 0 and not external_ca:
-+        # external CA step 1 doesn't have DS and KDC fully configured, yet
-         enable_replication_debugging(host)
-         setup_sssd_debugging(host)
-         kinit_admin(host)
-diff --git a/ipatests/test_integration/test_external_ca.py b/ipatests/test_integration/test_external_ca.py
-index 33ba70f98..a8e0ea0bf 100644
---- a/ipatests/test_integration/test_external_ca.py
-+++ b/ipatests/test_integration/test_external_ca.py
-@@ -70,24 +70,12 @@ def match_in_journal(host, string, since='today', services=('certmonger',)):
- 
- 
- def install_server_external_ca_step1(host):
--    """funtion for step 1 to install the ipa server with external ca"""
--
--    args = ['ipa-server-install', '-U',
--            '-a', host.config.admin_password,
--            '-p', host.config.dirman_password,
--            '--setup-dns', '--no-forwarders',
--            '-n', host.domain.name,
--            '-r', host.domain.realm,
--            '--domain-level=%i' % host.config.domain_level,
--            '--external-ca']
--
--    cmd = host.run_command(args)
--    return cmd
-+    """Step 1 to install the ipa server with external ca"""
-+    return tasks.install_master(host, external_ca=True)
- 
- 
- def install_server_external_ca_step2(host, ipa_ca_cert, root_ca_cert):
--    """funtion for step 2 to install the ipa server with external ca"""
--
-+    """Step 2 to install the ipa server with external ca"""
-     args = ['ipa-server-install',
-             '-a', host.config.admin_password,
-             '-p', host.config.dirman_password,
diff --git a/SOURCES/0006-external-ca-profile-fix_rhbz#1731813.patch b/SOURCES/0006-external-ca-profile-fix_rhbz#1731813.patch
new file mode 100644
index 0000000..ca70f28
--- /dev/null
+++ b/SOURCES/0006-external-ca-profile-fix_rhbz#1731813.patch
@@ -0,0 +1,1339 @@
+From d0d29ccc324bb9f95bffbe3162ee5c3c61c6086a Mon Sep 17 00:00:00 2001
+From: Fraser Tweedale <ftweedal@redhat.com>
+Date: Thu, 11 Jul 2019 15:17:04 +1000
+Subject: [PATCH] move MSCSTemplate classes to ipalib
+
+As we expand the integration tests for external CA functionality, it
+is helpful (and avoids duplication) to use the MSCSTemplate*
+classes.  These currently live in ipaserver.install.cainstance, but
+ipatests is no longer permitted to import from ipaserver (see commit
+81714976e5e13131654c78eb734746a20237c933).  So move these classes to
+ipalib.
+
+Part of: https://pagure.io/freeipa/issue/7548
+
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ install/tools/ipa-ca-install.in               |   6 +-
+ ipalib/x509.py                                | 171 +++++++++++++++++
+ ipaserver/install/ca.py                       |  11 +-
+ ipaserver/install/cainstance.py               | 180 +-----------------
+ ipaserver/install/ipa_cacert_manage.py        |  14 +-
+ ipatests/test_integration/test_external_ca.py |  11 +-
+ ipatests/test_ipalib/test_x509.py             | 115 +++++++++++
+ .../test_install/test_cainstance.py           | 123 ------------
+ 8 files changed, 307 insertions(+), 324 deletions(-)
+ delete mode 100644 ipatests/test_ipaserver/test_install/test_cainstance.py
+
+diff --git a/install/tools/ipa-ca-install.in b/install/tools/ipa-ca-install.in
+index 0700c0c38b..ce6d5fcb52 100644
+--- a/install/tools/ipa-ca-install.in
++++ b/install/tools/ipa-ca-install.in
+@@ -37,7 +37,7 @@ from ipaserver.install import cainstance, service
+ from ipaserver.install import custodiainstance
+ from ipaserver.masters import find_providing_server
+ from ipapython import version
+-from ipalib import api
++from ipalib import api, x509
+ from ipalib.constants import DOMAIN_LEVEL_1
+ from ipapython.config import IPAOptionParser
+ from ipapython.ipa_log_manager import standard_logging_setup
+@@ -68,13 +68,13 @@ def parse_options():
+                       default=False, help="unattended installation never prompts the user")
+     parser.add_option("--external-ca", dest="external_ca", action="store_true",
+                       default=False, help="Generate a CSR to be signed by an external CA")
+-    ext_cas = tuple(x.value for x in cainstance.ExternalCAType)
++    ext_cas = tuple(x.value for x in x509.ExternalCAType)
+     parser.add_option("--external-ca-type", dest="external_ca_type",
+                       type="choice", choices=ext_cas,
+                       metavar="{{{0}}}".format(",".join(ext_cas)),
+                       help="Type of the external CA. Default: generic")
+     parser.add_option("--external-ca-profile", dest="external_ca_profile",
+-                      type='constructor', constructor=cainstance.ExternalCAProfile,
++                      type='constructor', constructor=x509.ExternalCAProfile,
+                       default=None, metavar="PROFILE-SPEC",
+                       help="Specify the certificate profile/template to use "
+                            "at the external CA")
+diff --git a/ipalib/x509.py b/ipalib/x509.py
+index ab3c5f553d..1f612a3797 100644
+--- a/ipalib/x509.py
++++ b/ipalib/x509.py
+@@ -34,6 +34,7 @@
+ import os
+ import binascii
+ import datetime
++import enum
+ import ipaddress
+ import ssl
+ import base64
+@@ -47,6 +48,7 @@
+     Encoding, PublicFormat, PrivateFormat, load_pem_private_key
+ )
+ import pyasn1
++import pyasn1.error
+ from pyasn1.type import univ, char, namedtype, tag
+ from pyasn1.codec.der import decoder, encoder
+ from pyasn1_modules import rfc2315, rfc2459
+@@ -745,3 +747,172 @@ def format_datetime(t):
+     if t.tzinfo is None:
+         t = t.replace(tzinfo=UTC())
+     return unicode(t.strftime("%a %b %d %H:%M:%S %Y %Z"))
++
++
++class ExternalCAType(enum.Enum):
++    GENERIC = 'generic'
++    MS_CS = 'ms-cs'
++
++
++class ExternalCAProfile:
++    """
++    An external CA profile configuration.  Currently the only
++    subclasses are for Microsoft CAs, for providing data in the
++    "Certificate Template" extension.
++
++    Constructing this class will actually return an instance of a
++    subclass.
++
++    Subclasses MUST set ``valid_for``.
++
++    """
++    def __init__(self, s=None):
++        self.unparsed_input = s
++
++    # Which external CA types is the data valid for?
++    # A set of VALUES of the ExternalCAType enum.
++    valid_for = set()
++
++    def __new__(cls, s=None):
++        """Construct the ExternalCAProfile value.
++
++        Return an instance of a subclass determined by
++        the format of the argument.
++
++        """
++        # we are directly constructing a subclass; instantiate
++        # it and be done
++        if cls is not ExternalCAProfile:
++            return super(ExternalCAProfile, cls).__new__(cls)
++
++        # construction via the base class; therefore the string
++        # argument is required, and is used to determine which
++        # subclass to construct
++        if s is None:
++            raise ValueError('string argument is required')
++
++        parts = s.split(':')
++
++        try:
++            # Is the first part on OID?
++            _oid = univ.ObjectIdentifier(parts[0])
++
++            # It is; construct a V2 template
++            # pylint: disable=too-many-function-args
++            return MSCSTemplateV2.__new__(MSCSTemplateV2, s)
++
++        except pyasn1.error.PyAsn1Error:
++            # It is not an OID; treat as a template name
++            # pylint: disable=too-many-function-args
++            return MSCSTemplateV1.__new__(MSCSTemplateV1, s)
++
++    def __getstate__(self):
++        return self.unparsed_input
++
++    def __setstate__(self, state):
++        # explicitly call __init__ method to initialise object
++        self.__init__(state)
++
++
++class MSCSTemplate(ExternalCAProfile):
++    """
++    An Microsoft AD-CS Template specifier.
++
++    Subclasses MUST set ext_oid.
++
++    Subclass constructors MUST set asn1obj.
++
++    """
++    valid_for = set([ExternalCAType.MS_CS.value])
++
++    ext_oid = None  # extension OID, as a Python str
++    asn1obj = None  # unencoded extension data
++
++    def get_ext_data(self):
++        """Return DER-encoded extension data."""
++        return encoder.encode(self.asn1obj)
++
++
++class MSCSTemplateV1(MSCSTemplate):
++    """
++    A v1 template specifier, per
++    https://msdn.microsoft.com/en-us/library/cc250011.aspx.
++
++    ::
++
++        CertificateTemplateName ::= SEQUENCE {
++           Name            UTF8String
++        }
++
++    But note that a bare BMPString is used in practice.
++
++    """
++    ext_oid = "1.3.6.1.4.1.311.20.2"
++
++    def __init__(self, s):
++        super(MSCSTemplateV1, self).__init__(s)
++        parts = s.split(':')
++        if len(parts) > 1:
++            raise ValueError(
++                "Cannot specify certificate template version when using name.")
++        self.asn1obj = char.BMPString(str(parts[0]))
++
++
++class MSCSTemplateV2(MSCSTemplate):
++    """
++    A v2 template specifier, per
++    https://msdn.microsoft.com/en-us/library/windows/desktop/aa378274(v=vs.85).aspx
++
++    ::
++
++        CertificateTemplate ::= SEQUENCE {
++            templateID              EncodedObjectID,
++            templateMajorVersion    TemplateVersion,
++            templateMinorVersion    TemplateVersion OPTIONAL
++        }
++
++        TemplateVersion ::= INTEGER (0..4294967295)
++
++    """
++    ext_oid = "1.3.6.1.4.1.311.21.7"
++
++    @staticmethod
++    def check_version_in_range(desc, n):
++        if n < 0 or n >= 2**32:
++            raise ValueError(
++                "Template {} version must be in range 0..4294967295"
++                .format(desc))
++
++    def __init__(self, s):
++        super(MSCSTemplateV2, self).__init__(s)
++
++        parts = s.split(':')
++
++        obj = CertificateTemplateV2()
++        if len(parts) < 2 or len(parts) > 3:
++            raise ValueError(
++                "Incorrect template specification; required format is: "
++                "<oid>:<majorVersion>[:<minorVersion>]")
++        try:
++            obj['templateID'] = univ.ObjectIdentifier(parts[0])
++
++            major = int(parts[1])
++            self.check_version_in_range("major", major)
++            obj['templateMajorVersion'] = major
++
++            if len(parts) > 2:
++                minor = int(parts[2])
++                self.check_version_in_range("minor", minor)
++                obj['templateMinorVersion'] = int(parts[2])
++
++        except pyasn1.error.PyAsn1Error:
++            raise ValueError("Could not parse certificate template specifier.")
++        self.asn1obj = obj
++
++
++class CertificateTemplateV2(univ.Sequence):
++    componentType = namedtype.NamedTypes(
++        namedtype.NamedType('templateID', univ.ObjectIdentifier()),
++        namedtype.NamedType('templateMajorVersion', univ.Integer()),
++        namedtype.OptionalNamedType('templateMinorVersion', univ.Integer())
++    )
+diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py
+index 6b040b311a..8fb5e3ec91 100644
+--- a/ipaserver/install/ca.py
++++ b/ipaserver/install/ca.py
+@@ -28,7 +28,7 @@
+ from ipaplatform.paths import paths
+ from ipaserver.install import installutils, certs
+ from ipaserver.install.replication import replica_conn_check
+-from ipalib import api, errors
++from ipalib import api, errors, x509
+ from ipapython.dn import DN
+ 
+ from . import conncheck, dogtag, cainstance
+@@ -216,8 +216,7 @@ def install_check(standalone, replica_config, options):
+                 paths.ROOT_IPA_CSR)
+ 
+         if not options.external_ca_type:
+-            options.external_ca_type = \
+-                cainstance.ExternalCAType.GENERIC.value
++            options.external_ca_type = x509.ExternalCAType.GENERIC.value
+ 
+         if options.external_ca_profile is not None:
+             # check that profile is valid for the external ca type
+@@ -478,13 +477,11 @@ class CAInstallInterface(dogtag.DogtagInstallInterface,
+     external_ca = master_install_only(external_ca)
+ 
+     external_ca_type = knob(
+-        cainstance.ExternalCAType, None,
+-        description="Type of the external CA",
+-    )
++        x509.ExternalCAType, None, description="Type of the external CA")
+     external_ca_type = master_install_only(external_ca_type)
+ 
+     external_ca_profile = knob(
+-        type=cainstance.ExternalCAProfile,
++        type=x509.ExternalCAProfile,
+         default=None,
+         description=(
+             "Specify the certificate profile/template to use at the "
+diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
+index 6e1fc724db..2295581870 100644
+--- a/ipaserver/install/cainstance.py
++++ b/ipaserver/install/cainstance.py
+@@ -26,7 +26,6 @@
+ import logging
+ 
+ import dbus
+-import enum
+ import ldap
+ import os
+ import pwd
+@@ -39,10 +38,6 @@
+ import tempfile
+ from configparser import RawConfigParser
+ 
+-from pyasn1.codec.der import encoder
+-from pyasn1.type import char, univ, namedtype
+-import pyasn1.error
+-
+ from ipalib import api
+ from ipalib import x509
+ from ipalib import errors
+@@ -80,11 +75,6 @@
+ ]
+ 
+ 
+-class ExternalCAType(enum.Enum):
+-    GENERIC = 'generic'
+-    MS_CS = 'ms-cs'
+-
+-
+ def check_ports():
+     """Check that dogtag ports (8080, 8443) are available.
+ 
+@@ -367,7 +357,7 @@ def configure_instance(self, host_name, dm_password, admin_password,
+         if ca_type is not None:
+             self.ca_type = ca_type
+         else:
+-            self.ca_type = ExternalCAType.GENERIC.value
++            self.ca_type = x509.ExternalCAType.GENERIC.value
+         self.external_ca_profile = external_ca_profile
+ 
+         self.no_db_setup = promote
+@@ -537,12 +527,12 @@ def __spawn_instance(self):
+                 pki_ca_signing_csr_path=self.csr_file,
+             )
+ 
+-            if self.ca_type == ExternalCAType.MS_CS.value:
++            if self.ca_type == x509.ExternalCAType.MS_CS.value:
+                 # Include MS template name extension in the CSR
+                 template = self.external_ca_profile
+                 if template is None:
+                     # default template name
+-                    template = MSCSTemplateV1(u"SubCA")
++                    template = x509.MSCSTemplateV1(u"SubCA")
+ 
+                 ext_data = binascii.hexlify(template.get_ext_data())
+                 cfg.update(
+@@ -2081,170 +2071,6 @@ def update_ipa_conf():
+         parser.write(f)
+ 
+ 
+-class ExternalCAProfile:
+-    """
+-    An external CA profile configuration.  Currently the only
+-    subclasses are for Microsoft CAs, for providing data in the
+-    "Certificate Template" extension.
+-
+-    Constructing this class will actually return an instance of a
+-    subclass.
+-
+-    Subclasses MUST set ``valid_for``.
+-
+-    """
+-    def __init__(self, s=None):
+-        self.unparsed_input = s
+-
+-    # Which external CA types is the data valid for?
+-    # A set of VALUES of the ExternalCAType enum.
+-    valid_for = set()
+-
+-    def __new__(cls, s=None):
+-        """Construct the ExternalCAProfile value.
+-
+-        Return an instance of a subclass determined by
+-        the format of the argument.
+-
+-        """
+-        # we are directly constructing a subclass; instantiate
+-        # it and be done
+-        if cls is not ExternalCAProfile:
+-            return super(ExternalCAProfile, cls).__new__(cls)
+-
+-        # construction via the base class; therefore the string
+-        # argument is required, and is used to determine which
+-        # subclass to construct
+-        if s is None:
+-            raise ValueError('string argument is required')
+-
+-        parts = s.split(':')
+-
+-        try:
+-            # Is the first part on OID?
+-            _oid = univ.ObjectIdentifier(parts[0])
+-
+-            # It is; construct a V2 template
+-            # pylint: disable=too-many-function-args
+-            return MSCSTemplateV2.__new__(MSCSTemplateV2, s)
+-
+-        except pyasn1.error.PyAsn1Error:
+-            # It is not an OID; treat as a template name
+-            # pylint: disable=too-many-function-args
+-            return MSCSTemplateV1.__new__(MSCSTemplateV1, s)
+-
+-    def __getstate__(self):
+-        return self.unparsed_input
+-
+-    def __setstate__(self, state):
+-        # explicitly call __init__ method to initialise object
+-        self.__init__(state)
+-
+-
+-class MSCSTemplate(ExternalCAProfile):
+-    """
+-    An Microsoft AD-CS Template specifier.
+-
+-    Subclasses MUST set ext_oid.
+-
+-    Subclass constructors MUST set asn1obj.
+-
+-    """
+-    valid_for = set([ExternalCAType.MS_CS.value])
+-
+-    ext_oid = None  # extension OID, as a Python str
+-    asn1obj = None  # unencoded extension data
+-
+-    def get_ext_data(self):
+-        """Return DER-encoded extension data."""
+-        return encoder.encode(self.asn1obj)
+-
+-
+-class MSCSTemplateV1(MSCSTemplate):
+-    """
+-    A v1 template specifier, per
+-    https://msdn.microsoft.com/en-us/library/cc250011.aspx.
+-
+-    ::
+-
+-        CertificateTemplateName ::= SEQUENCE {
+-           Name            UTF8String
+-        }
+-
+-    But note that a bare BMPString is used in practice.
+-
+-    """
+-    ext_oid = "1.3.6.1.4.1.311.20.2"
+-
+-    def __init__(self, s):
+-        super(MSCSTemplateV1, self).__init__(s)
+-        parts = s.split(':')
+-        if len(parts) > 1:
+-            raise ValueError(
+-                "Cannot specify certificate template version when using name.")
+-        self.asn1obj = char.BMPString(str(parts[0]))
+-
+-
+-class MSCSTemplateV2(MSCSTemplate):
+-    """
+-    A v2 template specifier, per
+-    https://msdn.microsoft.com/en-us/library/windows/desktop/aa378274(v=vs.85).aspx
+-
+-    ::
+-
+-        CertificateTemplate ::= SEQUENCE {
+-            templateID              EncodedObjectID,
+-            templateMajorVersion    TemplateVersion,
+-            templateMinorVersion    TemplateVersion OPTIONAL
+-        }
+-
+-        TemplateVersion ::= INTEGER (0..4294967295)
+-
+-    """
+-    ext_oid = "1.3.6.1.4.1.311.21.7"
+-
+-    @staticmethod
+-    def check_version_in_range(desc, n):
+-        if n < 0 or n >= 2**32:
+-            raise ValueError(
+-                "Template {} version must be in range 0..4294967295"
+-                .format(desc))
+-
+-    def __init__(self, s):
+-        super(MSCSTemplateV2, self).__init__(s)
+-
+-        parts = s.split(':')
+-
+-        obj = CertificateTemplateV2()
+-        if len(parts) < 2 or len(parts) > 3:
+-            raise ValueError(
+-                "Incorrect template specification; required format is: "
+-                "<oid>:<majorVersion>[:<minorVersion>]")
+-        try:
+-            obj['templateID'] = univ.ObjectIdentifier(parts[0])
+-
+-            major = int(parts[1])
+-            self.check_version_in_range("major", major)
+-            obj['templateMajorVersion'] = major
+-
+-            if len(parts) > 2:
+-                minor = int(parts[2])
+-                self.check_version_in_range("minor", minor)
+-                obj['templateMinorVersion'] = int(parts[2])
+-
+-        except pyasn1.error.PyAsn1Error:
+-            raise ValueError("Could not parse certificate template specifier.")
+-        self.asn1obj = obj
+-
+-
+-class CertificateTemplateV2(univ.Sequence):
+-    componentType = namedtype.NamedTypes(
+-        namedtype.NamedType('templateID', univ.ObjectIdentifier()),
+-        namedtype.NamedType('templateMajorVersion', univ.Integer()),
+-        namedtype.OptionalNamedType('templateMinorVersion', univ.Integer())
+-    )
+-
+-
+ if __name__ == "__main__":
+     standard_logging_setup("install.log")
+     ds = dsinstance.DsInstance()
+diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py
+index 3f113c35bf..37dcc2befa 100644
+--- a/ipaserver/install/ipa_cacert_manage.py
++++ b/ipaserver/install/ipa_cacert_manage.py
+@@ -65,7 +65,7 @@ def add_options(cls, parser):
+             "--external-ca", dest='self_signed',
+             action='store_false',
+             help="Sign the renewed certificate by external CA")
+-        ext_cas = tuple(x.value for x in cainstance.ExternalCAType)
++        ext_cas = tuple(x.value for x in x509.ExternalCAType)
+         renew_group.add_option(
+             "--external-ca-type", dest="external_ca_type",
+             type="choice", choices=ext_cas,
+@@ -73,7 +73,7 @@ def add_options(cls, parser):
+             help="Type of the external CA. Default: generic")
+         renew_group.add_option(
+             "--external-ca-profile", dest="external_ca_profile",
+-            type='constructor', constructor=cainstance.ExternalCAProfile,
++            type='constructor', constructor=x509.ExternalCAProfile,
+             default=None, metavar="PROFILE-SPEC",
+             help="Specify the certificate profile/template to use "
+                  "at the external CA")
+@@ -224,11 +224,11 @@ def renew_external_step_1(self, ca):
+         options = self.options
+ 
+         if not options.external_ca_type:
+-            options.external_ca_type = cainstance.ExternalCAType.GENERIC.value
++            options.external_ca_type = x509.ExternalCAType.GENERIC.value
+ 
+-        if options.external_ca_type == cainstance.ExternalCAType.MS_CS.value \
++        if options.external_ca_type == x509.ExternalCAType.MS_CS.value \
+                 and options.external_ca_profile is None:
+-            options.external_ca_profile = cainstance.MSCSTemplateV1(u"SubCA")
++            options.external_ca_profile = x509.MSCSTemplateV1(u"SubCA")
+ 
+         if options.external_ca_profile is not None:
+             # check that profile is valid for the external ca type
+@@ -352,11 +352,11 @@ def resubmit_request(self, ca=RENEWAL_CA_NAME, profile=None):
+         timeout = api.env.startup_timeout + 60
+ 
+         cm_profile = None
+-        if isinstance(profile, cainstance.MSCSTemplateV1):
++        if isinstance(profile, x509.MSCSTemplateV1):
+             cm_profile = profile.unparsed_input
+ 
+         cm_template = None
+-        if isinstance(profile, cainstance.MSCSTemplateV2):
++        if isinstance(profile, x509.MSCSTemplateV2):
+             cm_template = profile.unparsed_input
+ 
+         logger.debug("resubmitting certmonger request '%s'", self.request_id)
+diff --git a/ipatests/test_integration/test_external_ca.py b/ipatests/test_integration/test_external_ca.py
+index a42355217d..5aa2b7bba0 100644
+--- a/ipatests/test_integration/test_external_ca.py
++++ b/ipatests/test_integration/test_external_ca.py
+@@ -108,14 +108,14 @@ def check_ipaca_issuerDN(host, expected_dn):
+     assert "Issuer DN: {}".format(expected_dn) in result.stdout_text
+ 
+ 
+-def check_mscs_extension(ipa_csr, oid, value):
++def check_mscs_extension(ipa_csr, template):
+     csr = x509.load_pem_x509_csr(ipa_csr, default_backend())
+     extensions = [
+         ext for ext in csr.extensions
+-        if ext.oid.dotted_string == oid
++        if ext.oid.dotted_string == template.ext_oid
+     ]
+     assert extensions
+-    assert extensions[0].value.value == value
++    assert extensions[0].value.value == template.get_ext_data()
+ 
+ 
+ class TestExternalCA(IntegrationTest):
+@@ -134,10 +134,7 @@ def test_external_ca(self):
+ 
+         # check CSR for extension
+         ipa_csr = self.master.get_file_contents(paths.ROOT_IPA_CSR)
+-        # Values for MSCSTemplateV1('SubCA')
+-        oid = "1.3.6.1.4.1.311.20.2"
+-        value = b'\x1e\n\x00S\x00u\x00b\x00C\x00A'
+-        check_mscs_extension(ipa_csr, oid, value)
++        check_mscs_extension(ipa_csr, ipa_x509.MSCSTemplateV1(u'SubCA'))
+ 
+         # Sign CA, transport it to the host and get ipa a root ca paths.
+         root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport(
+diff --git a/ipatests/test_ipalib/test_x509.py b/ipatests/test_ipalib/test_x509.py
+index ff7e6de2f7..284b998316 100644
+--- a/ipatests/test_ipalib/test_x509.py
++++ b/ipatests/test_ipalib/test_x509.py
+@@ -22,7 +22,11 @@
+ """
+ 
+ import base64
++from binascii import hexlify
++from configparser import RawConfigParser
+ import datetime
++from io import StringIO
++import pickle
+ 
+ import pytest
+ 
+@@ -268,3 +272,114 @@ def test_ipa_demo_letsencrypt(self):
+             b'0 \x06\x03U\x1d%\x01\x01\xff\x04\x160\x14\x06\x08+\x06\x01'
+             b'\x05\x05\x07\x03\x01\x06\x08+\x06\x01\x05\x05\x07\x03\x02'
+         )
++
++
++class test_ExternalCAProfile:
++    def test_MSCSTemplateV1_good(self):
++        o = x509.MSCSTemplateV1("MySubCA")
++        assert hexlify(o.get_ext_data()) == b'1e0e004d007900530075006200430041'
++
++    def test_MSCSTemplateV1_bad(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV1("MySubCA:1")
++
++    def test_MSCSTemplateV1_pickle_roundtrip(self):
++        o = x509.MSCSTemplateV1("MySubCA")
++        s = pickle.dumps(o)
++        assert o.get_ext_data() == pickle.loads(s).get_ext_data()
++
++    def test_MSCSTemplateV2_too_few_parts(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("1.2.3.4")
++
++    def test_MSCSTemplateV2_too_many_parts(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("1.2.3.4:100:200:300")
++
++    def test_MSCSTemplateV2_bad_oid(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("not_an_oid:1")
++
++    def test_MSCSTemplateV2_non_numeric_major_version(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("1.2.3.4:major:200")
++
++    def test_MSCSTemplateV2_non_numeric_minor_version(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("1.2.3.4:100:minor")
++
++    def test_MSCSTemplateV2_major_version_lt_zero(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("1.2.3.4:-1:200")
++
++    def test_MSCSTemplateV2_minor_version_lt_zero(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("1.2.3.4:100:-1")
++
++    def test_MSCSTemplateV2_major_version_gt_max(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("1.2.3.4:4294967296:200")
++
++    def test_MSCSTemplateV2_minor_version_gt_max(self):
++        with pytest.raises(ValueError):
++            x509.MSCSTemplateV2("1.2.3.4:100:4294967296")
++
++    def test_MSCSTemplateV2_good_major(self):
++        o = x509.MSCSTemplateV2("1.2.3.4:4294967295")
++        assert hexlify(o.get_ext_data()) == b'300c06032a0304020500ffffffff'
++
++    def test_MSCSTemplateV2_good_major_minor(self):
++        o = x509.MSCSTemplateV2("1.2.3.4:4294967295:0")
++        assert hexlify(o.get_ext_data()) \
++            == b'300f06032a0304020500ffffffff020100'
++
++    def test_MSCSTemplateV2_pickle_roundtrip(self):
++        o = x509.MSCSTemplateV2("1.2.3.4:4294967295:0")
++        s = pickle.dumps(o)
++        assert o.get_ext_data() == pickle.loads(s).get_ext_data()
++
++    def test_ExternalCAProfile_dispatch(self):
++        """
++        Test that constructing ExternalCAProfile actually returns an
++        instance of the appropriate subclass.
++        """
++        assert isinstance(
++            x509.ExternalCAProfile("MySubCA"),
++            x509.MSCSTemplateV1)
++        assert isinstance(
++            x509.ExternalCAProfile("1.2.3.4:100"),
++            x509.MSCSTemplateV2)
++
++    def test_write_pkispawn_config_file_MSCSTemplateV1(self):
++        template = x509.MSCSTemplateV1(u"SubCA")
++        expected = (
++            '[CA]\n'
++            'pki_req_ext_oid = 1.3.6.1.4.1.311.20.2\n'
++            'pki_req_ext_data = 1e0a00530075006200430041\n\n'
++        )
++        self._test_write_pkispawn_config_file(template, expected)
++
++    def test_write_pkispawn_config_file_MSCSTemplateV2(self):
++        template = x509.MSCSTemplateV2(u"1.2.3.4:4294967295")
++        expected = (
++            '[CA]\n'
++            'pki_req_ext_oid = 1.3.6.1.4.1.311.21.7\n'
++            'pki_req_ext_data = 300c06032a0304020500ffffffff\n\n'
++        )
++        self._test_write_pkispawn_config_file(template, expected)
++
++    def _test_write_pkispawn_config_file(self, template, expected):
++        """
++        Test that the values we read from an ExternalCAProfile
++        object can be used to produce a reasonable-looking pkispawn
++        configuration.
++        """
++        config = RawConfigParser()
++        config.optionxform = str
++        config.add_section("CA")
++        config.set("CA", "pki_req_ext_oid", template.ext_oid)
++        config.set("CA", "pki_req_ext_data",
++                   hexlify(template.get_ext_data()).decode('ascii'))
++        out = StringIO()
++        config.write(out)
++        assert out.getvalue() == expected
+diff --git a/ipatests/test_ipaserver/test_install/test_cainstance.py b/ipatests/test_ipaserver/test_install/test_cainstance.py
+deleted file mode 100644
+index 02d9758e4a..0000000000
+--- a/ipatests/test_ipaserver/test_install/test_cainstance.py
++++ /dev/null
+@@ -1,123 +0,0 @@
+-#
+-# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+-#
+-
+-from binascii import hexlify
+-from io import StringIO
+-import pickle
+-from configparser import RawConfigParser
+-import pytest
+-from ipaserver.install import cainstance
+-
+-pytestmark = pytest.mark.tier0
+-
+-
+-class test_ExternalCAProfile:
+-    def test_MSCSTemplateV1_good(self):
+-        o = cainstance.MSCSTemplateV1("MySubCA")
+-        assert hexlify(o.get_ext_data()) == b'1e0e004d007900530075006200430041'
+-
+-    def test_MSCSTemplateV1_bad(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV1("MySubCA:1")
+-
+-    def test_MSCSTemplateV1_pickle_roundtrip(self):
+-        o = cainstance.MSCSTemplateV1("MySubCA")
+-        s = pickle.dumps(o)
+-        assert o.get_ext_data() == pickle.loads(s).get_ext_data()
+-
+-    def test_MSCSTemplateV2_too_few_parts(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("1.2.3.4")
+-
+-    def test_MSCSTemplateV2_too_many_parts(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("1.2.3.4:100:200:300")
+-
+-    def test_MSCSTemplateV2_bad_oid(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("not_an_oid:1")
+-
+-    def test_MSCSTemplateV2_non_numeric_major_version(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("1.2.3.4:major:200")
+-
+-    def test_MSCSTemplateV2_non_numeric_minor_version(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("1.2.3.4:100:minor")
+-
+-    def test_MSCSTemplateV2_major_version_lt_zero(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("1.2.3.4:-1:200")
+-
+-    def test_MSCSTemplateV2_minor_version_lt_zero(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("1.2.3.4:100:-1")
+-
+-    def test_MSCSTemplateV2_major_version_gt_max(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("1.2.3.4:4294967296:200")
+-
+-    def test_MSCSTemplateV2_minor_version_gt_max(self):
+-        with pytest.raises(ValueError):
+-            cainstance.MSCSTemplateV2("1.2.3.4:100:4294967296")
+-
+-    def test_MSCSTemplateV2_good_major(self):
+-        o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295")
+-        assert hexlify(o.get_ext_data()) == b'300c06032a0304020500ffffffff'
+-
+-    def test_MSCSTemplateV2_good_major_minor(self):
+-        o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295:0")
+-        assert hexlify(o.get_ext_data()) \
+-            == b'300f06032a0304020500ffffffff020100'
+-
+-    def test_MSCSTemplateV2_pickle_roundtrip(self):
+-        o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295:0")
+-        s = pickle.dumps(o)
+-        assert o.get_ext_data() == pickle.loads(s).get_ext_data()
+-
+-    def test_ExternalCAProfile_dispatch(self):
+-        """
+-        Test that constructing ExternalCAProfile actually returns an
+-        instance of the appropriate subclass.
+-        """
+-        assert isinstance(
+-            cainstance.ExternalCAProfile("MySubCA"),
+-            cainstance.MSCSTemplateV1)
+-        assert isinstance(
+-            cainstance.ExternalCAProfile("1.2.3.4:100"),
+-            cainstance.MSCSTemplateV2)
+-
+-    def test_write_pkispawn_config_file_MSCSTemplateV1(self):
+-        template = cainstance.MSCSTemplateV1(u"SubCA")
+-        expected = (
+-            '[CA]\n'
+-            'pki_req_ext_oid = 1.3.6.1.4.1.311.20.2\n'
+-            'pki_req_ext_data = 1e0a00530075006200430041\n\n'
+-        )
+-        self._test_write_pkispawn_config_file(template, expected)
+-
+-    def test_write_pkispawn_config_file_MSCSTemplateV2(self):
+-        template = cainstance.MSCSTemplateV2(u"1.2.3.4:4294967295")
+-        expected = (
+-            '[CA]\n'
+-            'pki_req_ext_oid = 1.3.6.1.4.1.311.21.7\n'
+-            'pki_req_ext_data = 300c06032a0304020500ffffffff\n\n'
+-        )
+-        self._test_write_pkispawn_config_file(template, expected)
+-
+-    def _test_write_pkispawn_config_file(self, template, expected):
+-        """
+-        Test that the values we read from an ExternalCAProfile
+-        object can be used to produce a reasonable-looking pkispawn
+-        configuration.
+-        """
+-        config = RawConfigParser()
+-        config.optionxform = str
+-        config.add_section("CA")
+-        config.set("CA", "pki_req_ext_oid", template.ext_oid)
+-        config.set("CA", "pki_req_ext_data",
+-                   hexlify(template.get_ext_data()).decode('ascii'))
+-        out = StringIO()
+-        config.write(out)
+-        assert out.getvalue() == expected
+From e632b220798833bcd65c6b266610c800ed0914d7 Mon Sep 17 00:00:00 2001
+From: Fraser Tweedale <ftweedal@redhat.com>
+Date: Fri, 12 Jul 2019 13:13:02 +1000
+Subject: [PATCH] install: fix --external-ca-profile option
+
+Commit dd47cfc75a69618f486abefb70f2649ebf8264e7 removed the ability
+to set pki_req_ext_oid and pki_req_ext_data in the pkispawn config.
+This results in the --external-ca-profile option never setting the
+requested values in the CSR (the default V1 template type specifying
+"SubCA" is always used).
+
+Remove relevant fields from both ipaca_default.ini and
+ipaca_customize.ini.  This allows the IPA framework to set the
+values (i.e. when --external-ca-type=ms-cs and
+--external-ca-profile=... demand it).  It also allows users to
+override the pki_req_ext_* settings.
+
+Part of: https://pagure.io/freeipa/issue/7548
+Related: https://pagure.io/freeipa/issue/5608
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ install/share/ipaca_customize.ini | 5 -----
+ install/share/ipaca_default.ini   | 1 -
+ 2 files changed, 6 deletions(-)
+
+diff --git a/install/share/ipaca_customize.ini b/install/share/ipaca_customize.ini
+index 130ec2c102..6d58579af8 100644
+--- a/install/share/ipaca_customize.ini
++++ b/install/share/ipaca_customize.ini
+@@ -93,11 +93,6 @@ pki_ca_signing_key_type=%(ipa_ca_key_type)s
+ pki_ca_signing_signing_algorithm=%(ipa_ca_signing_algorithm)s
+ pki_ca_signing_token=%(pki_token_name)s
+ 
+-# MS subca request ext data
+-pki_req_ext_oid=1.3.6.1.4.1.311.20.2
+-pki_req_ext_critical=False
+-pki_req_ext_data=1E0A00530075006200430041
+-
+ ## ocspSigningCert cert-pki-ca
+ pki_ocsp_signing_key_algorithm=%(ipa_key_algorithm)s
+ pki_ocsp_signing_key_size=%(ipa_key_size)s
+diff --git a/install/share/ipaca_default.ini b/install/share/ipaca_default.ini
+index fedc1b9a74..2b9900286e 100644
+--- a/install/share/ipaca_default.ini
++++ b/install/share/ipaca_default.ini
+@@ -115,7 +115,6 @@ pki_ca_starting_crl_number=0
+ 
+ pki_external=False
+ pki_external_step_two=False
+-pki_req_ext_add=False
+ 
+ pki_external_pkcs12_path=%(pki_pkcs12_path)s
+ pki_external_pkcs12_password=%(pki_pkcs12_password)s
+From 71af731b3069fa1b2c0b51a3b917b5bc4da54350 Mon Sep 17 00:00:00 2001
+From: Fraser Tweedale <ftweedal@redhat.com>
+Date: Fri, 12 Jul 2019 13:24:51 +1000
+Subject: [PATCH] Fix use of incorrect variable
+
+Part of: https://pagure.io/freeipa/issue/7548
+Related: https://pagure.io/freeipa/issue/5608
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ ipaserver/install/dogtaginstance.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py
+index cc75d89746..5dca721d6c 100644
+--- a/ipaserver/install/dogtaginstance.py
++++ b/ipaserver/install/dogtaginstance.py
+@@ -853,7 +853,7 @@ def _verify_immutable(self, config, immutable_settings, filename):
+         if errs:
+             raise ValueError(
+                 '{} overrides immutable options:\n{}'.format(
+-                    filename, '\n'.join(errors)
++                    filename, '\n'.join(errs)
+                 )
+             )
+ 
+From 83ed05725110de19a7098678274ecaaaf6a2c9c9 Mon Sep 17 00:00:00 2001
+From: Fraser Tweedale <frase@frase.id.au>
+Date: Wed, 20 Feb 2019 18:34:33 +1100
+Subject: [PATCH] Add more tests for --external-ca-profile handling
+
+Add tests for remaining untested scenarios of --external-ca-profile
+handling in ipa-server-install.
+
+ipa-ca-install and ipa-cacert-manage remain untested at present.
+
+Fixes: https://pagure.io/freeipa/issue/7548
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ ipatests/test_integration/test_external_ca.py | 97 ++++++++++++++++++-
+ 1 file changed, 95 insertions(+), 2 deletions(-)
+
+diff --git a/ipatests/test_integration/test_external_ca.py b/ipatests/test_integration/test_external_ca.py
+index 5aa2b7bba0..dc9a09b43b 100644
+--- a/ipatests/test_integration/test_external_ca.py
++++ b/ipatests/test_integration/test_external_ca.py
+@@ -74,10 +74,10 @@ def match_in_journal(host, string, since='today', services=('certmonger',)):
+     return match
+ 
+ 
+-def install_server_external_ca_step1(host, extra_args=()):
++def install_server_external_ca_step1(host, extra_args=(), raiseonerr=True):
+     """Step 1 to install the ipa server with external ca"""
+     return tasks.install_master(
+-        host, external_ca=True, extra_args=extra_args
++        host, external_ca=True, extra_args=extra_args, raiseonerr=raiseonerr,
+     )
+ 
+ 
+@@ -478,3 +478,96 @@ def test_master_install_ca2(self):
+             'certutil', '-L', '-d', paths.PKI_TOMCAT_ALIAS_DIR,
+             '-n', cert_nick])
+         assert "CN=RootCA2" in result.stdout_text
++
++
++def _step1_profile(master, s):
++    return install_server_external_ca_step1(
++        master,
++        extra_args=['--external-ca-type=ms-cs', f'--external-ca-profile={s}'],
++        raiseonerr=False,
++    )
++
++
++def _test_invalid_profile(master, profile):
++    result = _step1_profile(master, profile)
++    assert result.returncode != 0
++    assert '--external-ca-profile' in result.stderr_text
++
++
++def _test_valid_profile(master, profile_cls, profile):
++    result = _step1_profile(master, profile)
++    assert result.returncode == 0
++    ipa_csr = master.get_file_contents(paths.ROOT_IPA_CSR)
++    check_mscs_extension(ipa_csr, profile_cls(profile))
++
++
++class TestExternalCAProfileV1(IntegrationTest):
++    """
++    Test that --external-ca-profile=Foo gets propagated to the CSR.
++
++    The default template extension when --external-ca-type=ms-cs,
++    a V1 extension with value "SubCA", already gets tested by the
++    ``TestExternalCA`` class.
++
++    We only need to do Step 1 of installation, then check the CSR.
++
++    """
++    def test_invalid_v1_template(self):
++        _test_invalid_profile(self.master, 'NotAnOid:1')
++
++    def test_valid_v1_template(self):
++        _test_valid_profile(
++            self.master, ipa_x509.MSCSTemplateV1, 'TemplateOfAwesome')
++
++
++class TestExternalCAProfileV2MajorOnly(IntegrationTest):
++    """
++    Test that V2 template specifiers without minor version get
++    propagated to CSR.  This class also tests all error modes in
++    specifying a V2 template, those being:
++
++    - no major version specified
++    - too many parts specified (i.e. major, minor, and then some more)
++    - major version is not an int
++    - major version is negative
++    - minor version is not an int
++    - minor version is negative
++
++    We only need to do Step 1 of installation, then check the CSR.
++
++    """
++    def test_v2_template_too_few_parts(self):
++        _test_invalid_profile(self.master, '1.2.3.4')
++
++    def test_v2_template_too_many_parts(self):
++        _test_invalid_profile(self.master, '1.2.3.4:100:200:300')
++
++    def test_v2_template_major_version_not_int(self):
++        _test_invalid_profile(self.master, '1.2.3.4:wat:200')
++
++    def test_v2_template_major_version_negative(self):
++        _test_invalid_profile(self.master, '1.2.3.4:-1:200')
++
++    def test_v2_template_minor_version_not_int(self):
++        _test_invalid_profile(self.master, '1.2.3.4:100:wat')
++
++    def test_v2_template_minor_version_negative(self):
++        _test_invalid_profile(self.master, '1.2.3.4:100:-2')
++
++    def test_v2_template_valid_major_only(self):
++        _test_valid_profile(
++            self.master, ipa_x509.MSCSTemplateV2, '1.2.3.4:100')
++
++
++class TestExternalCAProfileV2MajorMinor(IntegrationTest):
++    """
++    Test that V2 template specifiers _with_ minor version get
++    propagated to CSR.  All error modes of V2 template specifiers
++    were tested in ``TestExternalCAProfileV2Major``.
++
++    We only need to do Step 1 of installation, then check the CSR.
++
++    """
++    def test_v2_template_valid_major_minor(self):
++        _test_valid_profile(
++            self.master, ipa_x509.MSCSTemplateV2, '1.2.3.4:100:200')
+From a627df87c31e4d8399bd9fab43c0c4772ddd8955 Mon Sep 17 00:00:00 2001
+From: Fraser Tweedale <ftweedal@redhat.com>
+Date: Thu, 11 Jul 2019 20:22:33 +1000
+Subject: [PATCH] Collapse --external-ca-profile tests into single class
+
+To avoid having to spawn new CI hosts for each kind of
+--external-ca-profile argument we are testing, collapse the three
+separate test classes into one.  Uninstall the half-installed IPA
+after each section of tests.
+
+This change is in response to review comment
+https://github.com/freeipa/freeipa/pull/2852#pullrequestreview-220442170.
+
+Part of: https://pagure.io/freeipa/issue/7548
+
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ ipatests/test_integration/test_external_ca.py | 34 ++++++++++++++-----
+ 1 file changed, 26 insertions(+), 8 deletions(-)
+
+diff --git a/ipatests/test_integration/test_external_ca.py b/ipatests/test_integration/test_external_ca.py
+index dc9a09b43b..714aebd4a8 100644
+--- a/ipatests/test_integration/test_external_ca.py
++++ b/ipatests/test_integration/test_external_ca.py
+@@ -501,8 +501,18 @@ def _test_valid_profile(master, profile_cls, profile):
+     check_mscs_extension(ipa_csr, profile_cls(profile))
+ 
+ 
+-class TestExternalCAProfileV1(IntegrationTest):
++class TestExternalCAProfileScenarios(IntegrationTest):
+     """
++    Test the various --external-ca-profile scenarios.
++    This test is broken into sections, with each section first
++    testing invalid arguments, then a valid argument, and finally
++    uninstalling the half-installed IPA.
++
++    """
++
++    '''
++    Tranche 1: version 1 templates.
++
+     Test that --external-ca-profile=Foo gets propagated to the CSR.
+ 
+     The default template extension when --external-ca-type=ms-cs,
+@@ -511,7 +521,7 @@ class TestExternalCAProfileV1(IntegrationTest):
+ 
+     We only need to do Step 1 of installation, then check the CSR.
+ 
+-    """
++    '''
+     def test_invalid_v1_template(self):
+         _test_invalid_profile(self.master, 'NotAnOid:1')
+ 
+@@ -519,9 +529,12 @@ def test_valid_v1_template(self):
+         _test_valid_profile(
+             self.master, ipa_x509.MSCSTemplateV1, 'TemplateOfAwesome')
+ 
++    def test_uninstall_1(self):
++        tasks.uninstall_master(self.master)
++
++    '''
++    Tranche 2: V2 templates without minor version.
+ 
+-class TestExternalCAProfileV2MajorOnly(IntegrationTest):
+-    """
+     Test that V2 template specifiers without minor version get
+     propagated to CSR.  This class also tests all error modes in
+     specifying a V2 template, those being:
+@@ -535,7 +548,7 @@ class TestExternalCAProfileV2MajorOnly(IntegrationTest):
+ 
+     We only need to do Step 1 of installation, then check the CSR.
+ 
+-    """
++    '''
+     def test_v2_template_too_few_parts(self):
+         _test_invalid_profile(self.master, '1.2.3.4')
+ 
+@@ -558,16 +571,21 @@ def test_v2_template_valid_major_only(self):
+         _test_valid_profile(
+             self.master, ipa_x509.MSCSTemplateV2, '1.2.3.4:100')
+ 
++    def test_uninstall_2(self):
++        tasks.uninstall_master(self.master)
++
++    '''
++    Tranche 3: V2 templates with minor version.
+ 
+-class TestExternalCAProfileV2MajorMinor(IntegrationTest):
+-    """
+     Test that V2 template specifiers _with_ minor version get
+     propagated to CSR.  All error modes of V2 template specifiers
+     were tested in ``TestExternalCAProfileV2Major``.
+ 
+     We only need to do Step 1 of installation, then check the CSR.
+ 
+-    """
++    '''
+     def test_v2_template_valid_major_minor(self):
+         _test_valid_profile(
+             self.master, ipa_x509.MSCSTemplateV2, '1.2.3.4:100:200')
++
++    # this is the end; no need to uninstall.
+From 740964c3c47fd2cd216c233d8d9df1840eaa01ee Mon Sep 17 00:00:00 2001
+From: Fraser Tweedale <ftweedal@redhat.com>
+Date: Thu, 11 Jul 2019 20:27:02 +1000
+Subject: [PATCH] ci: add --external-ca-profile tests to nightly
+
+Part of: https://pagure.io/freeipa/issue/7548
+
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ ipatests/prci_definitions/nightly_f28.yaml        | 12 ++++++++++++
+ ipatests/prci_definitions/nightly_f29.yaml        | 12 ++++++++++++
+ ipatests/prci_definitions/nightly_master.yaml     | 12 ++++++++++++
+ ipatests/prci_definitions/nightly_master_pki.yaml | 12 ++++++++++++
+ ipatests/prci_definitions/nightly_rawhide.yaml    | 12 ++++++++++++
+ 5 files changed, 60 insertions(+)
+
+diff --git a/ipatests/prci_definitions/nightly_f28.yaml b/ipatests/prci_definitions/nightly_f28.yaml
+index fe86730444..d1605e6b5c 100644
+--- a/ipatests/prci_definitions/nightly_f28.yaml
++++ b/ipatests/prci_definitions/nightly_f28.yaml
+@@ -75,6 +75,18 @@ jobs:
+         timeout: 3600
+         topology: *master_1repl
+ 
++  fedora-28/external_ca_templates:
++    requires: [fedora-28/build]
++    priority: 50
++    job:
++      class: RunPytest
++      args:
++        build_url: '{fedora-28/build_url}'
++        test_suite: test_integration/test_external_ca.py::TestExternalCAProfileScenarios
++        template: *ci-master-f28
++        timeout: 3600
++        topology: *master_1repl
++
+   fedora-28/test_topologies:
+     requires: [fedora-28/build]
+     priority: 50
+diff --git a/ipatests/prci_definitions/nightly_f29.yaml b/ipatests/prci_definitions/nightly_f29.yaml
+index 57c1b624fe..ed88eb15c8 100644
+--- a/ipatests/prci_definitions/nightly_f29.yaml
++++ b/ipatests/prci_definitions/nightly_f29.yaml
+@@ -75,6 +75,18 @@ jobs:
+         timeout: 3600
+         topology: *master_1repl
+ 
++  fedora-29/external_ca_templates:
++    requires: [fedora-29/build]
++    priority: 50
++    job:
++      class: RunPytest
++      args:
++        build_url: '{fedora-29/build_url}'
++        test_suite: test_integration/test_external_ca.py::TestExternalCAProfileScenarios
++        template: *ci-master-f29
++        timeout: 3600
++        topology: *master_1repl
++
+   fedora-29/test_topologies:
+     requires: [fedora-29/build]
+     priority: 50
+diff --git a/ipatests/prci_definitions/nightly_master.yaml b/ipatests/prci_definitions/nightly_master.yaml
+index dc63f37426..0a66a13490 100644
+--- a/ipatests/prci_definitions/nightly_master.yaml
++++ b/ipatests/prci_definitions/nightly_master.yaml
+@@ -75,6 +75,18 @@ jobs:
+         timeout: 3600
+         topology: *master_1repl
+ 
++  fedora-30/external_ca_templates:
++    requires: [fedora-30/build]
++    priority: 50
++    job:
++      class: RunPytest
++      args:
++        build_url: '{fedora-30/build_url}'
++        test_suite: test_integration/test_external_ca.py::TestExternalCAProfileScenarios
++        template: *ci-master-f30
++        timeout: 3600
++        topology: *master_1repl
++
+   fedora-30/test_topologies:
+     requires: [fedora-30/build]
+     priority: 50
+diff --git a/ipatests/prci_definitions/nightly_master_pki.yaml b/ipatests/prci_definitions/nightly_master_pki.yaml
+index 1bb0af0244..ed2e38d3ed 100644
+--- a/ipatests/prci_definitions/nightly_master_pki.yaml
++++ b/ipatests/prci_definitions/nightly_master_pki.yaml
+@@ -75,6 +75,18 @@ jobs:
+         timeout: 3600
+         topology: *master_1repl
+ 
++  fedora-29/external_ca_templates:
++    requires: [fedora-29/build]
++    priority: 50
++    job:
++      class: RunPytest
++      args:
++        build_url: '{fedora-29/build_url}'
++        test_suite: test_integration/test_external_ca.py::TestExternalCAProfileScenarios
++        template: *pki-master-f29
++        timeout: 3600
++        topology: *master_1repl
++
+   fedora-29/test_vault:
+     requires: [fedora-29/build]
+     priority: 50
+diff --git a/ipatests/prci_definitions/nightly_rawhide.yaml b/ipatests/prci_definitions/nightly_rawhide.yaml
+index 301878467c..14433fcc0a 100644
+--- a/ipatests/prci_definitions/nightly_rawhide.yaml
++++ b/ipatests/prci_definitions/nightly_rawhide.yaml
+@@ -75,6 +75,18 @@ jobs:
+         timeout: 3600
+         topology: *master_1repl
+ 
++  fedora-rawhide/external_ca_templates:
++    requires: [fedora-rawhide/build]
++    priority: 50
++    job:
++      class: RunPytest
++      args:
++        build_url: '{fedora-rawhide/build_url}'
++        test_suite: test_integration/test_external_ca.py::TestExternalCAProfileScenarios
++        template: *ci-master-frawhide
++        timeout: 3600
++        topology: *master_1repl
++
+   fedora-rawhide/test_topologies:
+     requires: [fedora-rawhide/build]
+     priority: 50
+From 011c5283cec28ea4361eff5d2ee98da9cd3db41a Mon Sep 17 00:00:00 2001
+From: Fraser Tweedale <ftweedal@redhat.com>
+Date: Thu, 11 Jul 2019 20:27:02 +1000
+Subject: [PATCH] ci: add --external-ca-profile tests to gating
+
+Part of: https://pagure.io/freeipa/issue/7548
+
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ ipatests/prci_definitions/gating.yaml | 12 ++++++++++++
+ 1 file changed, 12 insertions(+)
+
+diff --git a/ipatests/prci_definitions/gating.yaml b/ipatests/prci_definitions/gating.yaml
+index 4d0107d956..81fa4bba10 100644
+--- a/ipatests/prci_definitions/gating.yaml
++++ b/ipatests/prci_definitions/gating.yaml
+@@ -87,6 +87,18 @@ jobs:
+         timeout: 3600
+         topology: *master_1repl
+ 
++  fedora-30/external_ca_templates:
++    requires: [fedora-30/build]
++    priority: 50
++    job:
++      class: RunPytest
++      args:
++        build_url: '{fedora-30/build_url}'
++        test_suite: test_integration/test_external_ca.py::TestExternalCAProfileScenarios
++        template: *ci-master-f30
++        timeout: 3600
++        topology: *master_1repl
++
+   fedora-30/test_topologies:
+     requires: [fedora-30/build]
+     priority: 50
diff --git a/SOURCES/0007-Allow-insecure-binds-for-migration-8e207fd3_rhbz#1731963.patch b/SOURCES/0007-Allow-insecure-binds-for-migration-8e207fd3_rhbz#1731963.patch
new file mode 100644
index 0000000..57b2ba4
--- /dev/null
+++ b/SOURCES/0007-Allow-insecure-binds-for-migration-8e207fd3_rhbz#1731963.patch
@@ -0,0 +1,72 @@
+From 8e207fd33d524f5cde2dfd8a41a08926a328a92b Mon Sep 17 00:00:00 2001
+From: Christian Heimes <cheimes@redhat.com>
+Date: Tue, 13 Aug 2019 17:22:01 +0200
+Subject: [PATCH] Allow insecure binds for migration
+
+Commit 5be9341fbabaf7bcb396a2ce40f17e1ccfa54b77 disallowed simple bind
+over an insecure connection. Password logins were only allowed over LDAPS
+or LDAP+STARTTLS. The restriction broke 'ipa migrate-ds' in some cases.
+
+This commit lifts the restriction and permits insecure binds over plain
+LDAP. It also makes the migrate-ds plugin use STARTTLS when a CA
+certificate is configured with a plain LDAP connection.
+
+Fixes: https://pagure.io/freeipa/issue/8040
+Signed-off-by: Christian Heimes <cheimes@redhat.com>
+Reviewed-By: Thomas Woerner <twoerner@redhat.com>
+---
+ ipapython/ipaldap.py           | 8 +++++---
+ ipaserver/plugins/migration.py | 9 ++++-----
+ 2 files changed, 9 insertions(+), 8 deletions(-)
+
+diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
+index 9ff443fe4f..f40858e27f 100644
+--- a/ipapython/ipaldap.py
++++ b/ipapython/ipaldap.py
+@@ -1206,12 +1206,14 @@ def _connect(self):
+         return conn
+ 
+     def simple_bind(self, bind_dn, bind_password, server_controls=None,
+-                    client_controls=None):
++                    client_controls=None, insecure_bind=False):
+         """
+         Perform simple bind operation.
+         """
+-        if self.protocol == 'ldap' and not self._start_tls and bind_password:
+-            # non-empty bind must use a secure connection
++        if (self.protocol == 'ldap' and not self._start_tls and
++                bind_password and not insecure_bind):
++            # non-empty bind must use a secure connection unless
++            # insecure bind is explicitly enabled
+             raise ValueError('simple_bind over insecure LDAP connection')
+         with self.error_handler():
+             self._flush_schema()
+diff --git a/ipaserver/plugins/migration.py b/ipaserver/plugins/migration.py
+index d0ca8369ae..b025c46cc5 100644
+--- a/ipaserver/plugins/migration.py
++++ b/ipaserver/plugins/migration.py
+@@ -901,20 +901,19 @@ def execute(self, ldapuri, bindpw, **options):
+             return dict(result={}, failed={}, enabled=False, compat=True)
+ 
+         # connect to DS
+-        cacert = None
+         if options.get('cacertfile') is not None:
+             # store CA cert into file
+             tmp_ca_cert_f = write_tmp_file(options['cacertfile'])
+             cacert = tmp_ca_cert_f.name
+ 
+-            # start TLS connection
+-            ds_ldap = LDAPClient(ldapuri, cacert=cacert)
++            # start TLS connection or STARTTLS
++            ds_ldap = LDAPClient(ldapuri, cacert=cacert, start_tls=True)
+             ds_ldap.simple_bind(options['binddn'], bindpw)
+ 
+             tmp_ca_cert_f.close()
+         else:
+-            ds_ldap = LDAPClient(ldapuri, cacert=cacert)
+-            ds_ldap.simple_bind(options['binddn'], bindpw)
++            ds_ldap = LDAPClient(ldapuri)
++            ds_ldap.simple_bind(options['binddn'], bindpw, insecure_bind=True)
+ 
+         # check whether the compat plugin is enabled
+         if not options.get('compat'):
diff --git a/SOURCES/0007-Replace-hard-coded-interpreter-with-sys.executable.patch b/SOURCES/0007-Replace-hard-coded-interpreter-with-sys.executable.patch
deleted file mode 100644
index 541a4c2..0000000
--- a/SOURCES/0007-Replace-hard-coded-interpreter-with-sys.executable.patch
+++ /dev/null
@@ -1,50 +0,0 @@
-From 64045c5dbaf24340dea5cf0bdb629c29f70a4a9d Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Wed, 24 Oct 2018 16:08:16 +0200
-Subject: [PATCH] Replace hard-coded interpreter with sys.executable
-
-Instead of hard-coding python3, the smart card advise script now uses
-the current executable path from sys.executable as interpreter.
-
-Fixes: https://pagure.io/freeipa/issue/7741
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-
-diff --git a/ipaserver/advise/plugins/smart_card_auth.py b/ipaserver/advise/plugins/smart_card_auth.py
-index 2f2e7aec9..97e23303b 100644
---- a/ipaserver/advise/plugins/smart_card_auth.py
-+++ b/ipaserver/advise/plugins/smart_card_auth.py
-@@ -4,6 +4,8 @@
- 
- from __future__ import absolute_import
- 
-+import sys
-+
- from ipalib.plugable import Registry
- from ipaplatform import services
- from ipaplatform.paths import paths
-@@ -186,9 +188,9 @@ class config_server_for_smart_card_auth(common_smart_card_auth_config):
-     def record_httpd_ocsp_status(self):
-         self.log.comment('store the OCSP upgrade state')
-         self.log.command(
--            "python3 -c 'from ipaserver.install import sysupgrade; "
-+            "{} -c 'from ipaserver.install import sysupgrade; "
-             "sysupgrade.set_upgrade_state(\"httpd\", "
--            "\"{}\", True)'".format(OCSP_ENABLED))
-+            "\"{}\", True)'".format(sys.executable, OCSP_ENABLED))
- 
-     def check_and_enable_pkinit(self):
-         self.log.comment('check whether PKINIT is configured on the master')
-@@ -310,10 +312,10 @@ class config_client_for_smart_card_auth(common_smart_card_auth_config):
-     def configure_pam_cert_auth(self):
-         self.log.comment('Set pam_cert_auth=True in /etc/sssd/sssd.conf')
-         self.log.command(
--            "python3 -c 'from SSSDConfig import SSSDConfig; "
-+            "{} -c 'from SSSDConfig import SSSDConfig; "
-             "c = SSSDConfig(); c.import_config(); "
-             "c.set(\"pam\", \"pam_cert_auth\", \"True\"); "
--            "c.write()'")
-+            "c.write()'".format(sys.executable))
- 
-     def restart_sssd(self):
-         self.log.command('systemctl restart sssd.service')
diff --git a/SOURCES/0008-Fix_misleading_errors_during_client_install_rollback_rhbz#1658283.patch b/SOURCES/0008-Fix_misleading_errors_during_client_install_rollback_rhbz#1658283.patch
deleted file mode 100644
index 6a9dfdf..0000000
--- a/SOURCES/0008-Fix_misleading_errors_during_client_install_rollback_rhbz#1658283.patch
+++ /dev/null
@@ -1,213 +0,0 @@
-From c64030a357401467d74e77d610d3bc268412220d Mon Sep 17 00:00:00 2001
-From: Rob Crittenden <rcritten@redhat.com>
-Date: Tue, 16 Oct 2018 13:58:00 -0400
-Subject: [PATCH] Remove the authselect profile warning if sssd was not
- configured.
-
-On a plain uninstall there should not be a bunch of confusing
-warning/error messages.
-
-Related to https://pagure.io/freeipa/issue/7729
-
-Signed-off-by: Rob Crittenden <rcritten@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- ipatests/test_integration/test_authselect.py | 1 -
- 1 file changed, 1 deletion(-)
-
-diff --git a/ipatests/test_integration/test_authselect.py b/ipatests/test_integration/test_authselect.py
-index 5eb3fdbf02..5ce56fa21e 100644
---- a/ipatests/test_integration/test_authselect.py
-+++ b/ipatests/test_integration/test_authselect.py
-@@ -136,7 +136,6 @@ def test_uninstall_client_no_preconfigured_profile(self):
-         # by default
-         result = self._uninstall_client()
-         assert result.returncode == 0
--        assert self.msg_warn_uninstall in result.stderr_text
-         check_authselect_profile(self.client, default_profile)
- 
-     def test_install_client_preconfigured_profile(self):
-From ec5e821f05cbc20517af6c9578e813f1963a9e8c Mon Sep 17 00:00:00 2001
-From: Rob Crittenden <rcritten@redhat.com>
-Date: Wed, 10 Oct 2018 14:07:33 -0400
-Subject: [PATCH] Fix misleading errors during client install rollback
-
-Some incorrect errors are possible if a client installation
-fails and a configuration rollback is required.
-
-These include:
-
-1. Unconfigured automount client failed: CalledProcessError(Command
-['/usr/sbin/ipa-client-automount', '--uninstall', '--debug']
-returned non-zero exit status 1: '')
-
-Caused by check_client_configuration() not returning the correct
-return value (2).
-
-2. WARNING: Unable to revert to the pre-installation state ('authconfig'
-tool has been deprecated in favor of 'authselect'). The default sssd
-profile will be used instead.
-The authconfig arguments would have been: authconfig --disableldap
---disablekrb5 --disablesssdauth --disablemkhomedir
-
-If installation fails before SSSD is configured there is no state
-to roll back to. Detect this condition.
-
-3. An error occurred while removing SSSD's cache.Please remove the
-cache manually by executing sssctl cache-remove -o.
-
-Again, if SSSD is not configured yet then there is no cache to
-remove. Also correct the missing space after the period.
-
-https://pagure.io/freeipa/issue/7729
-
-Signed-off-by: Rob Crittenden <rcritten@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- ipaclient/install/client.py                   | 18 ++++++-----
- ipalib/util.py                                |  5 +++-
- ipaplatform/redhat/authconfig.py              |  2 +-
- .../test_replica_promotion.py                 | 30 +++++++++++++++++++
- 4 files changed, 45 insertions(+), 10 deletions(-)
-
-diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
-index 0dcd1ec744..05255fe61b 100644
---- a/ipaclient/install/client.py
-+++ b/ipaclient/install/client.py
-@@ -3284,13 +3284,14 @@ def uninstall(options):
-     remove_file(paths.SSSD_MC_GROUP)
-     remove_file(paths.SSSD_MC_PASSWD)
- 
--    try:
--        run([paths.SSSCTL, "cache-remove", "-o", "--stop", "--start"])
--    except Exception:
--        logger.info(
--            "An error occurred while removing SSSD's cache."
--            "Please remove the cache manually by executing "
--            "sssctl cache-remove -o.")
-+    if was_sssd_installed:
-+        try:
-+            run([paths.SSSCTL, "cache-remove", "-o", "--stop", "--start"])
-+        except Exception:
-+            logger.info(
-+                "An error occurred while removing SSSD's cache."
-+                "Please remove the cache manually by executing "
-+                "sssctl cache-remove -o.")
- 
-     if ipa_domain:
-         sssd_domain_ldb = "cache_" + ipa_domain + ".ldb"
-@@ -3354,7 +3355,8 @@ def uninstall(options):
- 
-     # SSSD was not installed before our installation, and no other domains
-     # than IPA are configured in sssd.conf - make sure config file is removed
--    elif not was_sssd_installed and not was_sssd_configured:
-+    elif not was_sssd_installed and not was_sssd_configured \
-+            and os.path.exists(paths.SSSD_CONF):
-         try:
-             os.rename(paths.SSSD_CONF, paths.SSSD_CONF_DELETED)
-         except OSError:
-diff --git a/ipalib/util.py b/ipalib/util.py
-index 3e8fab49d6..68857baec7 100644
---- a/ipalib/util.py
-+++ b/ipalib/util.py
-@@ -1125,11 +1125,14 @@ def ensure_krbcanonicalname_set(ldap, entry_attrs):
- def check_client_configuration():
-     """
-     Check if IPA client is configured on the system.
-+
-+    Hardcode return code to avoid recursive imports
-     """
-     if (not os.path.isfile(paths.IPA_DEFAULT_CONF) or
-             not os.path.isdir(paths.IPA_CLIENT_SYSRESTORE) or
-             not os.listdir(paths.IPA_CLIENT_SYSRESTORE)):
--        raise ScriptError('IPA client is not configured on this system')
-+        raise ScriptError('IPA client is not configured on this system',
-+                          2)  # CLIENT_NOT_CONFIGURED
- 
- 
- def check_principal_realm_in_trust_namespace(api_instance, *keys):
-diff --git a/ipaplatform/redhat/authconfig.py b/ipaplatform/redhat/authconfig.py
-index ab3775e9e9..e456d9ec6e 100644
---- a/ipaplatform/redhat/authconfig.py
-+++ b/ipaplatform/redhat/authconfig.py
-@@ -141,7 +141,7 @@ def configure(self, sssd, mkhomedir, statestore, sudo=True):
-     def unconfigure(
-         self, fstore, statestore, was_sssd_installed, was_sssd_configured
-     ):
--        if not statestore.has_state('authselect'):
-+        if not statestore.has_state('authselect') and was_sssd_installed:
-             logger.warning(
-                 "WARNING: Unable to revert to the pre-installation state "
-                 "('authconfig' tool has been deprecated in favor of "
-diff --git a/ipatests/test_integration/test_replica_promotion.py b/ipatests/test_integration/test_replica_promotion.py
-index 265cbfb139..7803c34dcc 100644
---- a/ipatests/test_integration/test_replica_promotion.py
-+++ b/ipatests/test_integration/test_replica_promotion.py
-@@ -207,6 +207,36 @@ def test_upcase_client_domain(self):
-         assert(result1.returncode == 0), (
-             'Failed to promote the client installed with the upcase domain name')
- 
-+    def test_client_rollback(self):
-+        """Test that bogus error msgs are not in output on rollback.
-+
-+           FIXME: including in this suite to avoid setting up a
-+                  master just to test a client install failure. If
-+                  a pure client install suite is added this can be
-+                  moved.
-+
-+           Ticket https://pagure.io/freeipa/issue/7729
-+        """
-+        client = self.replicas[0]
-+
-+        # Cleanup previous run
-+        client.run_command(['ipa-server-install',
-+                            '--uninstall', '-U'], raiseonerr=False)
-+
-+        result = client.run_command(['ipa-client-install', '-U',
-+                                     '--server', self.master.hostname,
-+                                     '--domain', client.domain.name,
-+                                     '-w', 'foo'], raiseonerr=False)
-+
-+        assert(result.returncode == 1)
-+
-+        assert("Unconfigured automount client failed" not in
-+               result.stdout_text)
-+
-+        assert("WARNING: Unable to revert" not in result.stdout_text)
-+
-+        assert("An error occurred while removing SSSD" not in
-+               result.stdout_text)
- 
- class TestRenewalMaster(IntegrationTest):
- 
-From db960e32f155412c34807e204add4858090d3e94 Mon Sep 17 00:00:00 2001
-From: Rob Crittenden <rcritten@redhat.com>
-Date: Tue, 16 Oct 2018 14:07:25 -0400
-Subject: [PATCH] Collect the client and server uninstall logs in tests
-
-When running the integration tests capture the uninstallation
-logs as well as the installation logs.
-
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- ipatests/pytest_ipa/integration/tasks.py | 3 ++-
- 1 file changed, 2 insertions(+), 1 deletion(-)
-
-diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
-index f0c61381b6..620ed28c96 100644
---- a/ipatests/pytest_ipa/integration/tasks.py
-+++ b/ipatests/pytest_ipa/integration/tasks.py
-@@ -69,11 +69,12 @@ def setup_server_logs_collecting(host):
- 
-     # IPA install logs
-     host.collect_log(paths.IPASERVER_INSTALL_LOG)
-+    host.collect_log(paths.IPASERVER_UNINSTALL_LOG)
-     host.collect_log(paths.IPACLIENT_INSTALL_LOG)
-+    host.collect_log(paths.IPACLIENT_UNINSTALL_LOG)
-     host.collect_log(paths.IPAREPLICA_INSTALL_LOG)
-     host.collect_log(paths.IPAREPLICA_CONNCHECK_LOG)
-     host.collect_log(paths.IPAREPLICA_CA_INSTALL_LOG)
--    host.collect_log(paths.IPACLIENT_INSTALL_LOG)
-     host.collect_log(paths.IPASERVER_KRA_INSTALL_LOG)
-     host.collect_log(paths.IPA_CUSTODIA_AUDIT_LOG)
- 
diff --git a/SOURCES/0008-install-Add-missing-scripts-to-app_DATA_rhbz#1741170.patch b/SOURCES/0008-install-Add-missing-scripts-to-app_DATA_rhbz#1741170.patch
new file mode 100644
index 0000000..4b5ecd9
--- /dev/null
+++ b/SOURCES/0008-install-Add-missing-scripts-to-app_DATA_rhbz#1741170.patch
@@ -0,0 +1,104 @@
+install/updates/30-ipservices.update from 39eaf2fa as it is not part of the
+release tarball of 4.8.0 but needed for 27586cb7:
+
+commit 39eaf2fab5e27bd12edfb2a24c439a8ea5fb26f0
+Author: Christian Heimes <cheimes@redhat.com>
+Date:   Fri Dec 7 13:08:49 2018 +0100
+
+    Add index and container for RFC 2307 IP services
+    
+    IPA doesn't officially support RFC 2307 IP services. However SSSD has a
+    nsswitch plugin to provide service lookups. The subtree search for
+    (&(ipserviceport=$PORT)(ipserviceprotocol=$SRV)(objectclass=ipservice)) in
+    cn=accounts,$SUFFIX has caused performance issues on large
+    installations.
+    
+    This patch introduced a dedicated container
+    cn=ipservices,cn=accounts,$SUFFIX for IP services for future use or 3rd
+    party extensions. SSSD will be change its search base in an upcoming
+    release, too.
+    
+    A new ipServicePort index is added to optimize searches for an IP
+    service by port. There is no index on ipServiceProtocol because the index
+    would have poor selectivity. An ipService entry has either 'tcp' or 'udp'
+    as protocol.
+    
+    Fixes: https://pagure.io/freeipa/issue/7797
+    See: https://pagure.io/freeipa/issue/7786
+    Signed-off-by: Christian Heimes <cheimes@redhat.com>
+    Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+
+diff --git a/install/updates/30-ipservices.update b/install/updates/30-ipservices.update
+new file mode 100644
+index 000000000..01a6d52f8
+--- /dev/null
++++ b/install/updates/30-ipservices.update
+@@ -0,0 +1,6 @@
++# container for RFC 2307 IP services
++
++dn: cn=ipservices,cn=accounts,$SUFFIX
++default: objectClass: top
++default: objectClass: nsContainer
++default: cn: ipservices
+install/updates/75-user-trust-attributes.update from c18ee9b6 as it is not
+part of the release tarball of 4.8.0 but needed for 27586cb7:
+
+commit c18ee9b641ddc1e6b52d0413caa1fb98ac13785d
+Author: Tibor Dudlák <tdudlak@redhat.com>
+Date:   Tue Apr 2 16:23:09 2019 +0200
+
+    Add SMB attributes for users
+    
+    SMB attributes are used by Samba domain controller when reporting
+    details about IPA users via LSA DCE RPC calls.
+    
+    Based on the initial work from the external plugin:
+    https://github.com/abbra/freeipa-user-trust-attributes
+    
+    Related: https://pagure.io/freeipa/issue/3999
+    
+    Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
+    Signed-off-by: Tibor Dudlák <tdudlak@redhat.com>
+    Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+    Reviewed-By: Tibor Dudlak <tdudlak@redhat.com>
+
+diff --git a/install/updates/75-user-trust-attributes.update b/install/updates/75-user-trust-attributes.update
+new file mode 100644
+index 000000000..43bb40c7d
+--- /dev/null
++++ b/install/updates/75-user-trust-attributes.update
+@@ -0,0 +1,5 @@
++# Add an explicit self-service ACI to allow writing to manage trust attributes
++# for the owner of the object
++dn: cn=users,cn=accounts,$SUFFIX
++add:aci:(targetattr = "ipantlogonscript || ipantprofilepath || ipanthomedirectory || ipanthomedirectorydrive")(version 3.0;acl "system:Allow trust agents to read user SMB attributes";allow (read) groupdn = "ldap:///cn=adtrust agents,cn=sysaccounts,cn=etc,$SUFFIX";)
++add:aci:(targetattr = "ipantlogonscript || ipantprofilepath || ipanthomedirectory || ipanthomedirectorydrive")(version 3.0;acl "selfservice:Users can manage their SMB attributes";allow (write) userdn = "ldap:///self";)
+commit 27586cb7ae32af191cb8a3c36fc8856957300f08
+Author: Timo Aaltonen <tjaalton@debian.org>
+Date:   Fri Aug 9 23:03:25 2019 +0300
+
+    install: Add missing scripts to app_DATA.
+    
+    Signed-off-by: Timo Aaltonen <tjaalton@debian.org>
+    Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+
+diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
+index bce8a56b1..68facbaf2 100644
+--- a/install/updates/Makefile.am
++++ b/install/updates/Makefile.am
+@@ -30,6 +30,7 @@ app_DATA =				\
+ 	21-ca_renewal_container.update	\
+ 	21-certstore_container.update	\
+ 	25-referint.update		\
++	30-ipservices.update		\
+ 	30-provisioning.update		\
+ 	30-s4u2proxy.update		\
+ 	37-locations.update		\
+@@ -63,6 +64,7 @@ app_DATA =				\
+ 	73-custodia.update		\
+ 	73-winsync.update		\
+ 	73-certmap.update		\
++	75-user-trust-attributes.update	\
+ 	80-schema_compat.update \
+ 	90-post_upgrade_plugins.update	\
+ 	$(NULL)
diff --git a/SOURCES/0009-extdom-unify-error-code-handling-especially-LDAP_NO_SUCH_OBJECT_rhbz#1741530.patch b/SOURCES/0009-extdom-unify-error-code-handling-especially-LDAP_NO_SUCH_OBJECT_rhbz#1741530.patch
new file mode 100644
index 0000000..237532d
--- /dev/null
+++ b/SOURCES/0009-extdom-unify-error-code-handling-especially-LDAP_NO_SUCH_OBJECT_rhbz#1741530.patch
@@ -0,0 +1,345 @@
+From 3bb72545fc337564e0843b0c72906a9a1e3f6a06 Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Fri, 14 Jun 2019 11:13:54 +0200
+Subject: [PATCH] extdom: unify error code handling especially
+ LDAP_NO_SUCH_OBJECT
+
+A return code LDAP_NO_SUCH_OBJECT will tell SSSD on the IPA client to
+remove the searched object from the cache. As a consequence
+LDAP_NO_SUCH_OBJECT should only be returned if the object really does
+not exists otherwise the data of existing objects might be removed form
+the cache of the clients causing unexpected behaviour like
+authentication errors.
+
+Currently some code-paths use LDAP_NO_SUCH_OBJECT as default error code.
+With this patch LDAP_NO_SUCH_OBJECT is only returned if the related
+lookup functions return ENOENT. Timeout related error code will lead to
+LDAP_TIMELIMIT_EXCEEDED and LDAP_OPERATIONS_ERROR is used as default
+error code.
+
+Fixes: https://pagure.io/freeipa/issue/8044
+Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+---
+ .../ipa-extdom-extop/back_extdom_sss_idmap.c  |  4 +-
+ .../ipa-extdom-extop/ipa_extdom_common.c      | 77 ++++++++++++++-----
+ .../ipa-extdom-extop/ipa_extdom_extop.c       |  2 +
+ 3 files changed, 61 insertions(+), 22 deletions(-)
+
+diff --git a/daemons/ipa-slapi-plugins/ipa-extdom-extop/back_extdom_sss_idmap.c b/daemons/ipa-slapi-plugins/ipa-extdom-extop/back_extdom_sss_idmap.c
+index ef552a9a37..163e8e1371 100644
+--- a/daemons/ipa-slapi-plugins/ipa-extdom-extop/back_extdom_sss_idmap.c
++++ b/daemons/ipa-slapi-plugins/ipa-extdom-extop/back_extdom_sss_idmap.c
+@@ -62,10 +62,10 @@ static enum nss_status __convert_sss_nss2nss_status(int errcode) {
+         return NSS_STATUS_SUCCESS;
+     case ENOENT:
+         return NSS_STATUS_NOTFOUND;
+-    case ETIME:
+-        /* fall-through */
+     case ERANGE:
+         return NSS_STATUS_TRYAGAIN;
++    case ETIME:
++        /* fall-through */
+     case ETIMEDOUT:
+         /* fall-through */
+     default:
+diff --git a/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_common.c b/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_common.c
+index 525487c9e4..65c723ce65 100644
+--- a/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_common.c
++++ b/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_common.c
+@@ -523,7 +523,7 @@ int pack_ber_user(struct ipa_extdom_ctx *ctx,
+         if (strcasecmp(locat+1, domain_name) == 0  ) {
+             locat[0] = '\0';
+         } else {
+-            ret = LDAP_NO_SUCH_OBJECT;
++            ret = LDAP_INVALID_SYNTAX;
+             goto done;
+         }
+     }
+@@ -568,10 +568,12 @@ int pack_ber_user(struct ipa_extdom_ctx *ctx,
+             ret = getgrgid_r_wrapper(ctx,
+                                      groups[c], &grp, &buf, &buf_len);
+             if (ret != 0) {
+-                if (ret == ENOMEM || ret == ERANGE) {
+-                    ret = LDAP_OPERATIONS_ERROR;
+-                } else {
++                if (ret == ENOENT) {
+                     ret = LDAP_NO_SUCH_OBJECT;
++                } else if (ret == ETIMEDOUT) {
++                    ret = LDAP_TIMELIMIT_EXCEEDED;
++                } else {
++                    ret = LDAP_OPERATIONS_ERROR;
+                 }
+                 goto done;
+             }
+@@ -634,7 +636,7 @@ int pack_ber_group(enum response_types response_type,
+         if (strcasecmp(locat+1, domain_name) == 0  ) {
+             locat[0] = '\0';
+         } else {
+-            ret = LDAP_NO_SUCH_OBJECT;
++            ret = LDAP_INVALID_SYNTAX;
+             goto done;
+         }
+     }
+@@ -836,6 +838,8 @@ static int handle_uid_request(struct ipa_extdom_ctx *ctx,
+                             || id_type == SSS_ID_TYPE_BOTH)) {
+             if (ret == ENOENT) {
+                 ret = LDAP_NO_SUCH_OBJECT;
++            } else if (ret == ETIMEDOUT || ret == ETIME) {
++                ret = LDAP_TIMELIMIT_EXCEEDED;
+             } else {
+                 set_err_msg(req, "Failed to lookup SID by UID");
+                 ret = LDAP_OPERATIONS_ERROR;
+@@ -847,10 +851,12 @@ static int handle_uid_request(struct ipa_extdom_ctx *ctx,
+     } else {
+         ret = getpwuid_r_wrapper(ctx, uid, &pwd, &buf, &buf_len);
+         if (ret != 0) {
+-            if (ret == ENOMEM || ret == ERANGE) {
+-                ret = LDAP_OPERATIONS_ERROR;
+-            } else {
++            if (ret == ENOENT) {
+                 ret = LDAP_NO_SUCH_OBJECT;
++            } else if (ret == ETIMEDOUT) {
++                ret = LDAP_TIMELIMIT_EXCEEDED;
++            } else {
++                ret = LDAP_OPERATIONS_ERROR;
+             }
+             goto done;
+         }
+@@ -862,6 +868,8 @@ static int handle_uid_request(struct ipa_extdom_ctx *ctx,
+                 set_err_msg(req, "Failed to read original data");
+                 if (ret == ENOENT) {
+                     ret = LDAP_NO_SUCH_OBJECT;
++                } else if (ret == ETIMEDOUT || ret == ETIME) {
++                    ret = LDAP_TIMELIMIT_EXCEEDED;
+                 } else {
+                     ret = LDAP_OPERATIONS_ERROR;
+                 }
+@@ -907,6 +915,8 @@ static int handle_gid_request(struct ipa_extdom_ctx *ctx,
+         if (ret != 0 || id_type != SSS_ID_TYPE_GID) {
+             if (ret == ENOENT) {
+                 ret = LDAP_NO_SUCH_OBJECT;
++            } else if (ret == ETIMEDOUT || ret == ETIME) {
++                ret = LDAP_TIMELIMIT_EXCEEDED;
+             } else {
+                 set_err_msg(req, "Failed to lookup SID by GID");
+                 ret = LDAP_OPERATIONS_ERROR;
+@@ -918,10 +928,12 @@ static int handle_gid_request(struct ipa_extdom_ctx *ctx,
+     } else {
+         ret = getgrgid_r_wrapper(ctx, gid, &grp, &buf, &buf_len);
+         if (ret != 0) {
+-            if (ret == ENOMEM || ret == ERANGE) {
+-                ret = LDAP_OPERATIONS_ERROR;
+-            } else {
++            if (ret == ENOENT) {
+                 ret = LDAP_NO_SUCH_OBJECT;
++            } else if (ret == ETIMEDOUT) {
++                ret = LDAP_TIMELIMIT_EXCEEDED;
++            } else {
++                ret = LDAP_OPERATIONS_ERROR;
+             }
+             goto done;
+         }
+@@ -933,6 +945,8 @@ static int handle_gid_request(struct ipa_extdom_ctx *ctx,
+                 set_err_msg(req, "Failed to read original data");
+                 if (ret == ENOENT) {
+                     ret = LDAP_NO_SUCH_OBJECT;
++                } else if (ret == ETIMEDOUT || ret == ETIME) {
++                    ret = LDAP_TIMELIMIT_EXCEEDED;
+                 } else {
+                     ret = LDAP_OPERATIONS_ERROR;
+                 }
+@@ -976,6 +990,8 @@ static int handle_cert_request(struct ipa_extdom_ctx *ctx,
+     if (ret != 0) {
+         if (ret == ENOENT) {
+             ret = LDAP_NO_SUCH_OBJECT;
++        } else if (ret == ETIMEDOUT || ret == ETIME) {
++            ret = LDAP_TIMELIMIT_EXCEEDED;
+         } else {
+             set_err_msg(req, "Failed to lookup name by certificate");
+             ret = LDAP_OPERATIONS_ERROR;
+@@ -1020,6 +1036,8 @@ static int handle_sid_request(struct ipa_extdom_ctx *ctx,
+     if (ret != 0) {
+         if (ret == ENOENT) {
+             ret = LDAP_NO_SUCH_OBJECT;
++        } else if (ret == ETIMEDOUT || ret == ETIME) {
++            ret = LDAP_TIMELIMIT_EXCEEDED;
+         } else {
+             set_err_msg(req, "Failed to lookup name by SID");
+             ret = LDAP_OPERATIONS_ERROR;
+@@ -1057,10 +1075,12 @@ static int handle_sid_request(struct ipa_extdom_ctx *ctx,
+     case SSS_ID_TYPE_BOTH:
+         ret = getpwnam_r_wrapper(ctx, fq_name, &pwd, &buf, &buf_len);
+         if (ret != 0) {
+-            if (ret == ENOMEM || ret == ERANGE) {
+-                ret = LDAP_OPERATIONS_ERROR;
+-            } else {
++            if (ret == ENOENT) {
+                 ret = LDAP_NO_SUCH_OBJECT;
++            } else if (ret == ETIMEDOUT) {
++                ret = LDAP_TIMELIMIT_EXCEEDED;
++            } else {
++                ret = LDAP_OPERATIONS_ERROR;
+             }
+             goto done;
+         }
+@@ -1072,6 +1092,8 @@ static int handle_sid_request(struct ipa_extdom_ctx *ctx,
+                 set_err_msg(req, "Failed to read original data");
+                 if (ret == ENOENT) {
+                     ret = LDAP_NO_SUCH_OBJECT;
++                } else if (ret == ETIMEDOUT || ret == ETIME) {
++                    ret = LDAP_TIMELIMIT_EXCEEDED;
+                 } else {
+                     ret = LDAP_OPERATIONS_ERROR;
+                 }
+@@ -1089,10 +1111,12 @@ static int handle_sid_request(struct ipa_extdom_ctx *ctx,
+     case SSS_ID_TYPE_GID:
+         ret = getgrnam_r_wrapper(ctx, fq_name, &grp, &buf, &buf_len);
+         if (ret != 0) {
+-            if (ret == ENOMEM || ret == ERANGE) {
+-                ret = LDAP_OPERATIONS_ERROR;
+-            } else {
++            if (ret == ENOENT) {
+                 ret = LDAP_NO_SUCH_OBJECT;
++            } else if (ret == ETIMEDOUT) {
++                ret = LDAP_TIMELIMIT_EXCEEDED;
++            } else {
++                ret = LDAP_OPERATIONS_ERROR;
+             }
+             goto done;
+         }
+@@ -1104,6 +1128,8 @@ static int handle_sid_request(struct ipa_extdom_ctx *ctx,
+                 set_err_msg(req, "Failed to read original data");
+                 if (ret == ENOENT) {
+                     ret = LDAP_NO_SUCH_OBJECT;
++                } else if (ret == ETIMEDOUT || ret == ETIME) {
++                    ret = LDAP_TIMELIMIT_EXCEEDED;
+                 } else {
+                     ret = LDAP_OPERATIONS_ERROR;
+                 }
+@@ -1167,6 +1193,8 @@ static int handle_name_request(struct ipa_extdom_ctx *ctx,
+         if (ret != 0) {
+             if (ret == ENOENT) {
+                 ret = LDAP_NO_SUCH_OBJECT;
++            } else if (ret == ETIMEDOUT || ret == ETIME) {
++                ret = LDAP_TIMELIMIT_EXCEEDED;
+             } else {
+                 set_err_msg(req, "Failed to lookup SID by name");
+                 ret = LDAP_OPERATIONS_ERROR;
+@@ -1190,6 +1218,8 @@ static int handle_name_request(struct ipa_extdom_ctx *ctx,
+                     set_err_msg(req, "Failed to read original data");
+                     if (ret == ENOENT) {
+                         ret = LDAP_NO_SUCH_OBJECT;
++                    } else if (ret == ETIMEDOUT || ret == ETIME) {
++                        ret = LDAP_TIMELIMIT_EXCEEDED;
+                     } else {
+                         ret = LDAP_OPERATIONS_ERROR;
+                     }
+@@ -1205,6 +1235,9 @@ static int handle_name_request(struct ipa_extdom_ctx *ctx,
+         } else if (ret == ENOMEM || ret == ERANGE) {
+             ret = LDAP_OPERATIONS_ERROR;
+             goto done;
++        } else if (ret == ETIMEDOUT) {
++            ret = LDAP_TIMELIMIT_EXCEEDED;
++            goto done;
+         } else { /* no user entry found */
+             /* according to the getpwnam() man page there are a couple of
+              * error codes which can indicate that the user was not found. To
+@@ -1212,10 +1245,12 @@ static int handle_name_request(struct ipa_extdom_ctx *ctx,
+              * errors. */
+             ret = getgrnam_r_wrapper(ctx, fq_name, &grp, &buf, &buf_len);
+             if (ret != 0) {
+-                if (ret == ENOMEM || ret == ERANGE) {
+-                    ret = LDAP_OPERATIONS_ERROR;
+-                } else {
++                if (ret == ENOENT) {
+                     ret = LDAP_NO_SUCH_OBJECT;
++                } else if (ret == ETIMEDOUT) {
++                    ret = LDAP_TIMELIMIT_EXCEEDED;
++                } else {
++                    ret = LDAP_OPERATIONS_ERROR;
+                 }
+                 goto done;
+             }
+@@ -1226,6 +1261,8 @@ static int handle_name_request(struct ipa_extdom_ctx *ctx,
+                                     || id_type == SSS_ID_TYPE_BOTH)) {
+                     if (ret == ENOENT) {
+                         ret = LDAP_NO_SUCH_OBJECT;
++                    } else if (ret == ETIMEDOUT || ret == ETIME) {
++                        ret = LDAP_TIMELIMIT_EXCEEDED;
+                     } else {
+                         set_err_msg(req, "Failed to read original data");
+                         ret = LDAP_OPERATIONS_ERROR;
+diff --git a/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_extop.c b/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_extop.c
+index 10d3f86eba..48fcecc1ee 100644
+--- a/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_extop.c
++++ b/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_extop.c
+@@ -242,6 +242,8 @@ static int ipa_extdom_extop(Slapi_PBlock *pb)
+     if (ret != LDAP_SUCCESS) {
+         if (ret == LDAP_NO_SUCH_OBJECT) {
+             rc = LDAP_NO_SUCH_OBJECT;
++        } else if (ret == LDAP_TIMELIMIT_EXCEEDED) {
++            rc = LDAP_TIMELIMIT_EXCEEDED;
+         } else {
+             rc = LDAP_OPERATIONS_ERROR;
+             err_msg = "Failed to handle the request.\n";
+From 0ead6f59732e8b3370c5d8d05acd29f2d56c52bb Mon Sep 17 00:00:00 2001
+From: Alexander Bokovoy <abokovoy@redhat.com>
+Date: Mon, 19 Aug 2019 10:15:50 +0300
+Subject: [PATCH] ipa-extdom-extop: test timed out getgrgid_r
+
+Simulate getgrgid_r() timeout when packing list of groups user is a
+member of in pack_ber_user().
+
+Related: https://pagure.io/freeipa/issue/8044
+Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+---
+ .../ipa_extdom_cmocka_tests.c                 | 29 +++++++++++++++++++
+ 1 file changed, 29 insertions(+)
+
+diff --git a/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_cmocka_tests.c b/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_cmocka_tests.c
+index 29699cfa39..1fa4c6af82 100644
+--- a/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_cmocka_tests.c
++++ b/daemons/ipa-slapi-plugins/ipa-extdom-extop/ipa_extdom_cmocka_tests.c
+@@ -493,6 +493,34 @@ void test_set_err_msg(void **state)
+ #define TEST_SID "S-1-2-3-4"
+ #define TEST_DOMAIN_NAME "DOMAIN"
+ 
++/* Always time out for test */
++static
++enum nss_status getgrgid_r_timeout(gid_t gid, struct group *result,
++                                   char *buffer, size_t buflen, int *errnop) {
++    return NSS_STATUS_UNAVAIL;
++}
++
++void test_pack_ber_user_timeout(void **state)
++{
++    int ret;
++    struct berval *resp_val = NULL;
++    struct test_data *test_data;
++    enum nss_status (*oldgetgrgid_r)(gid_t gid, struct group *result,
++                                     char *buffer, size_t buflen, int *errnop);
++
++    test_data = (struct test_data *) *state;
++
++    oldgetgrgid_r = test_data->ctx->nss_ctx->getgrgid_r;
++    test_data->ctx->nss_ctx->getgrgid_r = getgrgid_r_timeout;
++
++    ret = pack_ber_user(test_data->ctx, RESP_USER_GROUPLIST,
++                        TEST_DOMAIN_NAME, "member001", 12345, 54321,
++                        "gecos", "homedir", "shell", NULL, &resp_val);
++    test_data->ctx->nss_ctx->getgrgid_r = oldgetgrgid_r;
++    assert_int_equal(ret, LDAP_TIMELIMIT_EXCEEDED);
++    ber_bvfree(resp_val);
++}
++
+ char res_sid[] = {0x30, 0x0e, 0x0a, 0x01, 0x01, 0x04, 0x09, 0x53, 0x2d, 0x31, \
+                   0x2d, 0x32, 0x2d, 0x33, 0x2d, 0x34};
+ char res_nam[] = {0x30, 0x13, 0x0a, 0x01, 0x02, 0x30, 0x0e, 0x04, 0x06, 0x44, \
+@@ -614,6 +642,7 @@ void test_decode(void **state)
+ int main(int argc, const char *argv[])
+ {
+     const struct CMUnitTest tests[] = {
++        cmocka_unit_test(test_pack_ber_user_timeout),
+         cmocka_unit_test(test_getpwnam_r_wrapper),
+         cmocka_unit_test(test_getpwuid_r_wrapper),
+         cmocka_unit_test(test_getgrnam_r_wrapper),
diff --git a/SOURCES/0009-ipa-advise_update_url_of_cacerdir_rehash_tool_9cfd07e_rhbz#1658287.patch b/SOURCES/0009-ipa-advise_update_url_of_cacerdir_rehash_tool_9cfd07e_rhbz#1658287.patch
deleted file mode 100644
index 40b5db5..0000000
--- a/SOURCES/0009-ipa-advise_update_url_of_cacerdir_rehash_tool_9cfd07e_rhbz#1658287.patch
+++ /dev/null
@@ -1,55 +0,0 @@
-From 9cfd07e87964f37465dd699a50444e6953291f59 Mon Sep 17 00:00:00 2001
-From: Petr Vobornik <pvoborni@redhat.com>
-Date: Oct 23 2018 14:48:53 +0000
-Subject: ipa-advise: update url of cacerdir_rehash tool
-
-
-On legacy systems which don't have cacerdir_rehash tool (provided by authconfig)
-the generated advise script downloads this tool from project page and uses it.
-
-After decommision of Fedorahosted and move of authconfig project to Pagure,
-this url was not updated in FreeIPA project.
-
-This patch updates the url.
-
-https://pagure.io/freeipa/issue/7731
-
-Signed-off-by: Petr Vobornik <pvoborni@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/ipaserver/advise/plugins/legacy_clients.py b/ipaserver/advise/plugins/legacy_clients.py
-index 7916965..2a56922 100644
---- a/ipaserver/advise/plugins/legacy_clients.py
-+++ b/ipaserver/advise/plugins/legacy_clients.py
-@@ -28,6 +28,9 @@ from ipapython.ipautil import template_file
- 
- register = Registry()
- 
-+CACERTDIR_REHASH_URL = ('https://pagure.io/authconfig/raw/master/f/'
-+                        'cacertdir_rehash')
-+
- 
- class config_base_legacy_client(Advice):
-     def get_uri_and_base(self):
-@@ -50,8 +53,6 @@ class config_base_legacy_client(Advice):
-                          'location. If this value is different on your system '
-                          'the script needs to be modified accordingly.\n')
- 
--        cacertdir_rehash = ('https://fedorahosted.org/authconfig/browser/'
--                            'cacertdir_rehash?format=txt')
-         self.log.comment('Download the CA certificate of the IPA server')
-         self.log.command('mkdir -p -m 755 /etc/openldap/cacerts')
-         self.log.command('curl http://%s/ipa/config/ca.crt -o '
-@@ -60,7 +61,8 @@ class config_base_legacy_client(Advice):
-         self.log.comment('Generate hashes for the openldap library')
-         self.log.command('command -v cacertdir_rehash')
-         self.log.command('if [ $? -ne 0 ] ; then')
--        self.log.command(' curl "%s" -o cacertdir_rehash ;' % cacertdir_rehash)
-+        self.log.command(' curl "%s" -o cacertdir_rehash ;' %
-+                         CACERTDIR_REHASH_URL)
-         self.log.command(' chmod 755 ./cacertdir_rehash ;')
-         self.log.command(' ./cacertdir_rehash /etc/openldap/cacerts/ ;')
-         self.log.command('else')
-
diff --git a/SOURCES/0010-Fix-automount-behavior-with-authselect_rhbz#1740167.patch b/SOURCES/0010-Fix-automount-behavior-with-authselect_rhbz#1740167.patch
new file mode 100644
index 0000000..60e22bb
--- /dev/null
+++ b/SOURCES/0010-Fix-automount-behavior-with-authselect_rhbz#1740167.patch
@@ -0,0 +1,2004 @@
+From abea98a9b918c0771ad10b314238b32c570f0372 Mon Sep 17 00:00:00 2001
+From: François Cami <fcami@redhat.com>
+Date: Aug 29 2019 06:45:12 +0000
+Subject: ipatests: check that ipa-client-automount restores nsswitch.conf at uninstall time
+
+
+Check that using ipa-client-install, ipa-client-automount --no-ssd, then uninstalling
+both properly restores nsswitch.conf sequentially.
+
+Related-to:: https://pagure.io/freeipa/issue/8038
+Signed-off-by: François Cami <fcami@redhat.com>
+Reviewed-By: Francois Cami <fcami@redhat.com>
+Reviewed-By: Rob Crittenden <rcritten@redhat.com>
+Reviewed-By: Rob Critenden <rcritten@redhat.com>
+Reviewed-By: François Cami <fcami@redhat.com>
+
+---
+
+#diff --git a/ipatests/prci_definitions/nightly_ipa-4-8.yaml b/ipatests/prci_definitions/nightly_ipa-4-8.yaml
+#index ef5d2c6..f39e4b4 100644
+#--- a/ipatests/prci_definitions/nightly_ipa-4-8.yaml
+#+++ b/ipatests/prci_definitions/nightly_ipa-4-8.yaml
+#@@ -1257,6 +1257,18 @@ jobs:
+#         timeout: 9000
+#         topology: *master_3client
+# 
+#+  fedora-30/nfs_nsswitch_restore:
+#+    requires: [fedora-30/build]
+#+    priority: 50
+#+    job:
+#+      class: RunPytest
+#+      args:
+#+        build_url: '{fedora-30/build_url}'
+#+        test_suite: test_integration/test_nfs.py::TestIpaClientAutomountFileRestore
+#+        template: *ci-master-f30
+#+        timeout: 3600
+#+        topology: *master_3client
+#+
+#   fedora-30/mask:
+#     requires: [fedora-30/build]
+#     priority: 50
+diff --git a/ipatests/test_integration/test_nfs.py b/ipatests/test_integration/test_nfs.py
+index adfc19f..0e1ef6a 100644
+--- a/ipatests/test_integration/test_nfs.py
++++ b/ipatests/test_integration/test_nfs.py
+@@ -15,6 +15,7 @@
+ 
+ from __future__ import absolute_import
+ 
++import pytest
+ import os
+ import re
+ import time
+@@ -258,3 +259,74 @@ class TestNFS(IntegrationTest):
+         time.sleep(WAIT_AFTER_UNINSTALL)
+ 
+         self.cleanup()
++
++
++class TestIpaClientAutomountFileRestore(IntegrationTest):
++
++    num_clients = 1
++    topology = 'line'
++
++    @classmethod
++    def install(cls, mh):
++        tasks.install_master(cls.master, setup_dns=True)
++
++    def teardown_method(self, method):
++        tasks.uninstall_client(self.clients[0])
++
++    def nsswitch_backup_restore(
++        self,
++        no_sssd=False,
++    ):
++
++        # In order to get a more pure sum, one that ignores the Generated
++        # header and any white space we have to do a bit of work...
++        sha256nsswitch_cmd = \
++            'egrep -v "Generated|^$" /etc/nsswitch.conf | sed "s/\\s//g" ' \
++            '| sort | sha256sum'
++
++        cmd = self.clients[0].run_command(sha256nsswitch_cmd)
++        orig_sha256 = cmd.stdout_text
++
++        grep_automount_command = \
++            "grep automount /etc/nsswitch.conf | cut -d: -f2"
++
++        tasks.install_client(self.master, self.clients[0])
++        cmd = self.clients[0].run_command(grep_automount_command)
++        after_ipa_client_install = cmd.stdout_text.split()
++
++        if no_sssd:
++            ipa_client_automount_command = [
++                "ipa-client-automount", "--no-sssd", "-U"
++            ]
++        else:
++            ipa_client_automount_command = [
++                "ipa-client-automount", "-U"
++            ]
++        self.clients[0].run_command(ipa_client_automount_command)
++        cmd = self.clients[0].run_command(grep_automount_command)
++        after_ipa_client_automount = cmd.stdout_text.split()
++        if no_sssd:
++            assert after_ipa_client_automount == ['files', 'ldap']
++        else:
++            assert after_ipa_client_automount == ['sss', 'files']
++
++        cmd = self.clients[0].run_command(grep_automount_command)
++        assert cmd.stdout_text.split() == after_ipa_client_automount
++
++        self.clients[0].run_command([
++            "ipa-client-automount", "--uninstall", "-U"
++        ])
++
++        cmd = self.clients[0].run_command(grep_automount_command)
++        assert cmd.stdout_text.split() == after_ipa_client_install
++
++        tasks.uninstall_client(self.clients[0])
++        cmd = self.clients[0].run_command(sha256nsswitch_cmd)
++        assert cmd.stdout_text == orig_sha256
++
++    @pytest.mark.xfail(reason='freeipa ticket 8054', strict=True)
++    def test_nsswitch_backup_restore_sssd(self):
++        self.nsswitch_backup_restore()
++
++    def test_nsswitch_backup_restore_no_sssd(self):
++        self.nsswitch_backup_restore(no_sssd=True)
+
+From 2f0afeda6e66fcca5c184a4036112fcd315f2f6e Mon Sep 17 00:00:00 2001
+From: François Cami <fcami@redhat.com>
+Date: Aug 29 2019 06:45:12 +0000
+Subject: ipa-client-automount: always restore nsswitch.conf at uninstall time
+
+
+ipa-client-automount used to only restore nsswitch.conf when sssd was not
+used. However authselect's default profile is now sssd so always restore
+nsswitch.conf's automount configuration to 'files sssd'.
+Note that the behavior seen before commit:
+a0e846f56c8de3b549d1d284087131da13135e34
+would always restore nsswitch.conf to the previous state which in some cases
+was wrong.
+
+Fixes: https://pagure.io/freeipa/issue/8038
+Signed-off-by: François Cami <fcami@redhat.com>
+Reviewed-By: Francois Cami <fcami@redhat.com>
+Reviewed-By: Rob Crittenden <rcritten@redhat.com>
+Reviewed-By: Rob Critenden <rcritten@redhat.com>
+Reviewed-By: François Cami <fcami@redhat.com>
+
+---
+
+diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py
+index fa07598..a1dc2a1 100644
+--- a/ipaclient/install/ipa_client_automount.py
++++ b/ipaclient/install/ipa_client_automount.py
+@@ -177,18 +177,30 @@ def configure_xml(fstore):
+         print("Configured %s" % authconf)
+ 
+ 
+-def configure_nsswitch(fstore, options):
++def configure_nsswitch(statestore, options):
+     """
+-    Point automount to ldap in nsswitch.conf. This function is for non-SSSD
+-    setups only
++    Point automount to ldap in nsswitch.conf.
++    This function is for non-SSSD setups only.
+     """
+-    fstore.backup_file(paths.NSSWITCH_CONF)
+-
+     conf = ipachangeconf.IPAChangeConf("IPA Installer")
+     conf.setOptionAssignment(':')
+ 
+-    nss_value = ' files ldap'
++    with open(paths.NSSWITCH_CONF, 'r') as f:
++        current_opts = conf.parse(f)
++        current_nss_value = conf.findOpts(
++            current_opts, name='automount', type='option'
++        )[1]
++        if current_nss_value is None:
++            # no automount database present
++            current_nss_value = False  # None cannot be backed up
++        else:
++            current_nss_value = current_nss_value['value']
++        statestore.backup_state(
++            'ipa-client-automount-nsswitch', 'previous-automount',
++            current_nss_value
++        )
+ 
++    nss_value = ' files ldap'
+     opts = [
+         {
+             'name': 'automount',
+@@ -198,7 +210,6 @@ def configure_nsswitch(fstore, options):
+         },
+         {'name': 'empty', 'type': 'empty'},
+     ]
+-
+     conf.changeConf(paths.NSSWITCH_CONF, opts)
+ 
+     print("Configured %s" % paths.NSSWITCH_CONF)
+@@ -322,19 +333,47 @@ def configure_autofs_common(fstore, statestore, options):
+ def uninstall(fstore, statestore):
+     RESTORE_FILES = [
+         paths.SYSCONFIG_AUTOFS,
+-        paths.NSSWITCH_CONF,
+         paths.AUTOFS_LDAP_AUTH_CONF,
+         paths.SYSCONFIG_NFS,
+         paths.IDMAPD_CONF,
+     ]
+     STATES = ['autofs', 'rpcidmapd', 'rpcgssd']
+ 
+-    # automount only touches /etc/nsswitch.conf if LDAP is
+-    # used. Don't restore it otherwise.
+-    if statestore.get_state('authconfig', 'sssd') or (
+-        statestore.get_state('authselect', 'profile') == 'sssd'
+-    ):
+-        RESTORE_FILES.remove(paths.NSSWITCH_CONF)
++    if statestore.get_state(
++        'ipa-client-automount-nsswitch', 'previous-automount'
++    ) is False:
++        # Previous nsswitch.conf had no automount database configured
++        # so remove it.
++        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
++        conf.setOptionAssignment(':')
++        changes = [conf.rmOption('automount')]
++        conf.changeConf(paths.NSSWITCH_CONF, changes)
++        tasks.restore_context(paths.NSSWITCH_CONF)
++        statestore.delete_state(
++            'ipa-client-automount-nsswitch', 'previous-automount'
++        )
++    elif statestore.get_state(
++        'ipa-client-automount-nsswitch', 'previous-automount'
++    ) is not None:
++        nss_value = statestore.get_state(
++            'ipa-client-automount-nsswitch', 'previous-automount'
++        )
++        opts = [
++            {
++                'name': 'automount',
++                'type': 'option',
++                'action': 'set',
++                'value': nss_value,
++            },
++            {'name': 'empty', 'type': 'empty'},
++        ]
++        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
++        conf.setOptionAssignment(':')
++        conf.changeConf(paths.NSSWITCH_CONF, opts)
++        tasks.restore_context(paths.NSSWITCH_CONF)
++        statestore.delete_state(
++            'ipa-client-automount-nsswitch', 'previous-automount'
++        )
+ 
+     if not any(fstore.has_file(f) for f in RESTORE_FILES) or not any(
+         statestore.has_state(s) for s in STATES
+@@ -588,7 +627,7 @@ def configure_automount():
+ 
+     try:
+         if not options.sssd:
+-            configure_nsswitch(fstore, options)
++            configure_nsswitch(statestore, options)
+         configure_nfs(fstore, statestore, options)
+         if options.sssd:
+             configure_autofs_sssd(fstore, statestore, autodiscover, options)
+
+From 6e92776bfc199e9ca92e11ef3315dcecad3c9307 Mon Sep 17 00:00:00 2001
+From: Rob Critenden <rcritten@redhat.com>
+Date: Aug 29 2019 06:45:12 +0000
+Subject: Move ipachangeconf from ipaclient.install to ipapython
+
+
+This will let us call it from ipaplatform.
+
+Mark the original location as deprecated.
+
+Reviewed-By: Francois Cami <fcami@redhat.com>
+Reviewed-By: Rob Crittenden <rcritten@redhat.com>
+Reviewed-By: Rob Critenden <rcritten@redhat.com>
+Reviewed-By: François Cami <fcami@redhat.com>
+
+---
+
+diff --git a/install/tools/ipa-replica-conncheck.in b/install/tools/ipa-replica-conncheck.in
+index 9208076..b22db11 100644
+--- a/install/tools/ipa-replica-conncheck.in
++++ b/install/tools/ipa-replica-conncheck.in
+@@ -22,7 +22,7 @@ from __future__ import print_function
+ 
+ import logging
+ 
+-import ipaclient.install.ipachangeconf
++from ipapython import ipachangeconf
+ from ipapython.config import IPAOptionParser
+ from ipapython.dn import DN
+ from ipapython import version
+@@ -229,7 +229,7 @@ def sigterm_handler(signum, frame):
+ 
+ def configure_krb5_conf(realm, kdc, filename):
+ 
+-    krbconf = ipaclient.install.ipachangeconf.IPAChangeConf("IPA Installer")
++    krbconf = ipachangeconf.IPAChangeConf("IPA Installer")
+     krbconf.setOptionAssignment((" = ", " "))
+     krbconf.setSectionNameDelimiters(("[","]"))
+     krbconf.setSubSectionDelimiters(("{","}"))
+diff --git a/ipaclient/install/ipachangeconf.py b/ipaclient/install/ipachangeconf.py
+index a13e0ea..c51e42e 100644
+--- a/ipaclient/install/ipachangeconf.py
++++ b/ipaclient/install/ipachangeconf.py
+@@ -18,566 +18,18 @@
+ # along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ #
+ 
+-import fcntl
+-import logging
+-import os
+-import shutil
++import warnings
++from ipapython.ipachangeconf import IPAChangeConf as realIPAChangeConf
+ 
+-import six
+ 
+-if six.PY3:
+-    unicode = str
++class IPAChangeConf(realIPAChangeConf):
++    """Advertise the old name"""
+ 
+-logger = logging.getLogger(__name__)
+-
+-def openLocked(filename, perms):
+-    fd = -1
+-    try:
+-        fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms)
+-
+-        fcntl.lockf(fd, fcntl.LOCK_EX)
+-    except OSError as e:
+-        if fd != -1:
+-            try:
+-                os.close(fd)
+-            except OSError:
+-                pass
+-        raise IOError(e.errno, e.strerror)
+-    return os.fdopen(fd, "r+")
+-
+-
+-    #TODO: add subsection as a concept
+-    #      (ex. REALM.NAME = { foo = x bar = y } )
+-    #TODO: put section delimiters as separating element of the list
+-    #      so that we can process multiple sections in one go
+-    #TODO: add a comment all but provided options as a section option
+-class IPAChangeConf:
+     def __init__(self, name):
+-        self.progname = name
+-        self.indent = ("", "", "")
+-        self.assign = (" = ", "=")
+-        self.dassign = self.assign[0]
+-        self.comment = ("#",)
+-        self.dcomment = self.comment[0]
+-        self.eol = ("\n",)
+-        self.deol = self.eol[0]
+-        self.sectnamdel = ("[", "]")
+-        self.subsectdel = ("{", "}")
+-        self.case_insensitive_sections = True
+-
+-    def setProgName(self, name):
+-        self.progname = name
+-
+-    def setIndent(self, indent):
+-        if type(indent) is tuple:
+-            self.indent = indent
+-        elif type(indent) is str:
+-            self.indent = (indent, )
+-        else:
+-            raise ValueError('Indent must be a list of strings')
+-
+-    def setOptionAssignment(self, assign):
+-        if type(assign) is tuple:
+-            self.assign = assign
+-        else:
+-            self.assign = (assign, )
+-        self.dassign = self.assign[0]
+-
+-    def setCommentPrefix(self, comment):
+-        if type(comment) is tuple:
+-            self.comment = comment
+-        else:
+-            self.comment = (comment, )
+-        self.dcomment = self.comment[0]
+-
+-    def setEndLine(self, eol):
+-        if type(eol) is tuple:
+-            self.eol = eol
+-        else:
+-            self.eol = (eol, )
+-        self.deol = self.eol[0]
+-
+-    def setSectionNameDelimiters(self, delims):
+-        self.sectnamdel = delims
+-
+-    def setSubSectionDelimiters(self, delims):
+-        self.subsectdel = delims
+-
+-    def matchComment(self, line):
+-        for v in self.comment:
+-            if line.lstrip().startswith(v):
+-                return line.lstrip()[len(v):]
+-        return False
+-
+-    def matchEmpty(self, line):
+-        if line.strip() == "":
+-            return True
+-        return False
+-
+-    def matchSection(self, line):
+-        cl = "".join(line.strip().split())
+-        cl = cl.lower() if self.case_insensitive_sections else cl
+-
+-        if len(self.sectnamdel) != 2:
+-            return False
+-        if not cl.startswith(self.sectnamdel[0]):
+-            return False
+-        if not cl.endswith(self.sectnamdel[1]):
+-            return False
+-        return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])]
+-
+-    def matchSubSection(self, line):
+-        if self.matchComment(line):
+-            return False
+-
+-        parts = line.split(self.dassign, 1)
+-        if len(parts) < 2:
+-            return False
+-
+-        if parts[1].strip() == self.subsectdel[0]:
+-            return parts[0].strip()
+-
+-        return False
+-
+-    def matchSubSectionEnd(self, line):
+-        if self.matchComment(line):
+-            return False
+-
+-        if line.strip() == self.subsectdel[1]:
+-            return True
+-
+-        return False
+-
+-    def getSectionLine(self, section):
+-        if len(self.sectnamdel) != 2:
+-            return section
+-        return self._dump_line(self.sectnamdel[0],
+-                               section,
+-                               self.sectnamdel[1],
+-                               self.deol)
+-
+-    def _dump_line(self, *args):
+-        return u"".join(unicode(x) for x in args)
+-
+-    def dump(self, options, level=0):
+-        output = []
+-        if level >= len(self.indent):
+-            level = len(self.indent) - 1
+-
+-        for o in options:
+-            if o['type'] == "section":
+-                output.append(self._dump_line(self.sectnamdel[0],
+-                                              o['name'],
+-                                              self.sectnamdel[1]))
+-                output.append(self.dump(o['value'], (level + 1)))
+-                continue
+-            if o['type'] == "subsection":
+-                output.append(self._dump_line(self.indent[level],
+-                                              o['name'],
+-                                              self.dassign,
+-                                              self.subsectdel[0]))
+-                output.append(self.dump(o['value'], (level + 1)))
+-                output.append(self._dump_line(self.indent[level],
+-                                              self.subsectdel[1]))
+-                continue
+-            if o['type'] == "option":
+-                delim = o.get('delim', self.dassign)
+-                if delim not in self.assign:
+-                    raise ValueError('Unknown delim "%s" must be one of "%s"' % (delim, " ".join([d for d in self.assign])))
+-                output.append(self._dump_line(self.indent[level],
+-                                              o['name'],
+-                                              delim,
+-                                              o['value']))
+-                continue
+-            if o['type'] == "comment":
+-                output.append(self._dump_line(self.dcomment, o['value']))
+-                continue
+-            if o['type'] == "empty":
+-                output.append('')
+-                continue
+-            raise SyntaxError('Unknown type: [%s]' % o['type'])
+-
+-        # append an empty string to the output so that we add eol to the end
+-        # of the file contents in a single join()
+-        output.append('')
+-        return self.deol.join(output)
+-
+-    def parseLine(self, line):
+-
+-        if self.matchEmpty(line):
+-            return {'name': 'empty', 'type': 'empty'}
+-
+-        value = self.matchComment(line)
+-        if value:
+-            return {'name': 'comment',
+-                    'type': 'comment',
+-                    'value': value.rstrip()}  # pylint: disable=E1103
+-
+-        o = dict()
+-        parts = line.split(self.dassign, 1)
+-        if len(parts) < 2:
+-            # The default assign didn't match, try the non-default
+-            for d in self.assign[1:]:
+-                parts = line.split(d, 1)
+-                if len(parts) >= 2:
+-                    o['delim'] = d
+-                    break
+-
+-            if 'delim' not in o:
+-                raise SyntaxError('Syntax Error: Unknown line format')
+-
+-        o.update({'name':parts[0].strip(), 'type':'option', 'value':parts[1].rstrip()})
+-        return o
+-
+-    def findOpts(self, opts, type, name, exclude_sections=False):
+-
+-        num = 0
+-        for o in opts:
+-            if o['type'] == type and o['name'] == name:
+-                return (num, o)
+-            if exclude_sections and (o['type'] == "section" or
+-                                     o['type'] == "subsection"):
+-                return (num, None)
+-            num += 1
+-        return (num, None)
+-
+-    def commentOpts(self, inopts, level=0):
+-
+-        opts = []
+-
+-        if level >= len(self.indent):
+-            level = len(self.indent) - 1
+-
+-        for o in inopts:
+-            if o['type'] == 'section':
+-                no = self.commentOpts(o['value'], (level + 1))
+-                val = self._dump_line(self.dcomment,
+-                                      self.sectnamdel[0],
+-                                      o['name'],
+-                                      self.sectnamdel[1])
+-                opts.append({'name': 'comment',
+-                             'type': 'comment',
+-                             'value': val})
+-                for n in no:
+-                    opts.append(n)
+-                continue
+-            if o['type'] == 'subsection':
+-                no = self.commentOpts(o['value'], (level + 1))
+-                val = self._dump_line(self.indent[level],
+-                                      o['name'],
+-                                      self.dassign,
+-                                      self.subsectdel[0])
+-                opts.append({'name': 'comment',
+-                             'type': 'comment',
+-                             'value': val})
+-                opts.extend(no)
+-                val = self._dump_line(self.indent[level], self.subsectdel[1])
+-                opts.append({'name': 'comment',
+-                             'type': 'comment',
+-                             'value': val})
+-                continue
+-            if o['type'] == 'option':
+-                delim = o.get('delim', self.dassign)
+-                if delim not in self.assign:
+-                    val = self._dump_line(self.indent[level],
+-                                          o['name'],
+-                                          delim,
+-                                          o['value'])
+-                opts.append({'name':'comment', 'type':'comment', 'value':val})
+-                continue
+-            if o['type'] == 'comment':
+-                opts.append(o)
+-                continue
+-            if o['type'] == 'empty':
+-                opts.append({'name': 'comment',
+-                             'type': 'comment',
+-                             'value': ''})
+-                continue
+-            raise SyntaxError('Unknown type: [%s]' % o['type'])
+-
+-        return opts
+-
+-    def mergeOld(self, oldopts, newopts):
+-
+-        opts = []
+-
+-        for o in oldopts:
+-            if o['type'] == "section" or o['type'] == "subsection":
+-                _num, no = self.findOpts(newopts, o['type'], o['name'])
+-                if not no:
+-                    opts.append(o)
+-                    continue
+-                if no['action'] == "set":
+-                    mo = self.mergeOld(o['value'], no['value'])
+-                    opts.append({'name': o['name'],
+-                                 'type': o['type'],
+-                                 'value': mo})
+-                    continue
+-                if no['action'] == "comment":
+-                    co = self.commentOpts(o['value'])
+-                    for c in co:
+-                        opts.append(c)
+-                    continue
+-                if no['action'] == "remove":
+-                    continue
+-                raise SyntaxError('Unknown action: [%s]' % no['action'])
+-
+-            if o['type'] == "comment" or o['type'] == "empty":
+-                opts.append(o)
+-                continue
+-
+-            if o['type'] == "option":
+-                _num, no = self.findOpts(newopts, 'option', o['name'], True)
+-                if not no:
+-                    opts.append(o)
+-                    continue
+-                if no['action'] == 'comment' or no['action'] == 'remove':
+-                    if (no['value'] is not None and
+-                            o['value'] is not no['value']):
+-                        opts.append(o)
+-                        continue
+-                    if no['action'] == 'comment':
+-                        value = self._dump_line(self.dcomment,
+-                                                o['name'],
+-                                                self.dassign,
+-                                                o['value'])
+-                        opts.append({'name': 'comment',
+-                                     'type': 'comment',
+-                                     'value': value})
+-                    continue
+-                if no['action'] == 'set':
+-                    opts.append(no)
+-                    continue
+-                if no['action'] == 'addifnotset':
+-                    opts.append({
+-                        'name': 'comment',
+-                        'type': 'comment',
+-                        'value': self._dump_line(
+-                            ' ', no['name'], ' modified by IPA'
+-                        ),
+-                    })
+-                    opts.append({'name': 'comment', 'type': 'comment',
+-                                'value': self._dump_line(no['name'],
+-                                                         self.dassign,
+-                                                         no['value'],
+-                                                         )})
+-                    opts.append(o)
+-                    continue
+-                raise SyntaxError('Unknown action: [%s]' % no['action'])
+-
+-            raise SyntaxError('Unknown type: [%s]' % o['type'])
+-
+-        return opts
+-
+-    def mergeNew(self, opts, newopts):
+-
+-        cline = 0
+-
+-        for no in newopts:
+-
+-            if no['type'] == "section" or no['type'] == "subsection":
+-                (num, o) = self.findOpts(opts, no['type'], no['name'])
+-                if not o:
+-                    if no['action'] == 'set':
+-                        opts.append(no)
+-                    continue
+-                if no['action'] == "set":
+-                    self.mergeNew(o['value'], no['value'])
+-                    continue
+-                cline = num + 1
+-                continue
+-
+-            if no['type'] == "option":
+-                (num, o) = self.findOpts(opts, no['type'], no['name'], True)
+-                if not o:
+-                    if no['action'] == 'set' or no['action'] == 'addifnotset':
+-                        opts.append(no)
+-                    continue
+-                cline = num + 1
+-                continue
+-
+-            if no['type'] == "comment" or no['type'] == "empty":
+-                opts.insert(cline, no)
+-                cline += 1
+-                continue
+-
+-            raise SyntaxError('Unknown type: [%s]' % no['type'])
+-
+-    def merge(self, oldopts, newopts):
+-        """
+-        Uses a two pass strategy:
+-        First we create a new opts tree from oldopts removing/commenting
+-          the options as indicated by the contents of newopts
+-        Second we fill in the new opts tree with options as indicated
+-          in the newopts tree (this is becaus eentire (sub)sections may
+-          in the newopts tree (this is becaus entire (sub)sections may
+-          exist in the newopts that do not exist in oldopts)
+-        """
+-        opts = self.mergeOld(oldopts, newopts)
+-        self.mergeNew(opts, newopts)
+-        return opts
+-
+-    #TODO: Make parse() recursive?
+-    def parse(self, f):
+-
+-        opts = []
+-        sectopts = []
+-        section = None
+-        subsectopts = []
+-        subsection = None
+-        curopts = opts
+-        fatheropts = opts
+-
+-        # Read in the old file.
+-        for line in f:
+-
+-            # It's a section start.
+-            value = self.matchSection(line)
+-            if value:
+-                if section is not None:
+-                    opts.append({'name': section,
+-                                 'type': 'section',
+-                                 'value': sectopts})
+-                sectopts = []
+-                curopts = sectopts
+-                fatheropts = sectopts
+-                section = value
+-                continue
+-
+-            # It's a subsection start.
+-            value = self.matchSubSection(line)
+-            if value:
+-                if subsection is not None:
+-                    raise SyntaxError('nested subsections are not '
+-                                      'supported yet')
+-                subsectopts = []
+-                curopts = subsectopts
+-                subsection = value
+-                continue
+-
+-            value = self.matchSubSectionEnd(line)
+-            if value:
+-                if subsection is None:
+-                    raise SyntaxError('Unmatched end subsection terminator '
+-                                      'found')
+-                fatheropts.append({'name': subsection,
+-                                   'type': 'subsection',
+-                                   'value': subsectopts})
+-                subsection = None
+-                curopts = fatheropts
+-                continue
+-
+-            # Copy anything else as is.
+-            try:
+-                curopts.append(self.parseLine(line))
+-            except SyntaxError as e:
+-                raise SyntaxError('{error} in file {fname}: [{line}]'.format(
+-                    error=e, fname=f.name, line=line.rstrip()))
+-
+-        #Add last section if any
+-        if len(sectopts) is not 0:
+-            opts.append({'name': section,
+-                         'type': 'section',
+-                         'value': sectopts})
+-
+-        return opts
+-
+-    def changeConf(self, file, newopts):
+-        """
+-        Write settings to configuration file
+-        :param file: path to the file
+-        :param options: set of dictionaries in the form:
+-             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
+-        :param section: section name like 'global'
+-        """
+-        output = ""
+-        f = None
+-        try:
+-            # Do not catch an unexisting file error
+-            # we want to fail in that case
+-            shutil.copy2(file, (file + ".ipabkp"))
+-
+-            f = openLocked(file, 0o644)
+-
+-            oldopts = self.parse(f)
+-
+-            options = self.merge(oldopts, newopts)
+-
+-            output = self.dump(options)
+-
+-            # Write it out and close it.
+-            f.seek(0)
+-            f.truncate(0)
+-            f.write(output)
+-        finally:
+-            try:
+-                if f:
+-                    f.close()
+-            except IOError:
+-                pass
+-        logger.debug("Updating configuration file %s", file)
+-        logger.debug(output)
+-        return True
+-
+-    def newConf(self, file, options, file_perms=0o644):
+-        """"
+-        Write settings to a new file, backup the old
+-        :param file: path to the file
+-        :param options: a set of dictionaries in the form:
+-             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
+-        :param file_perms: number defining the new file's permissions
+-        """
+-        output = ""
+-        f = None
+-        try:
+-            try:
+-                shutil.copy2(file, (file + ".ipabkp"))
+-            except IOError as err:
+-                if err.errno == 2:
+-                    # The orign file did not exist
+-                    pass
+-
+-            f = openLocked(file, file_perms)
+-
+-            # Trunkate
+-            f.seek(0)
+-            f.truncate(0)
+-
+-            output = self.dump(options)
+-
+-            f.write(output)
+-        finally:
+-            try:
+-                if f:
+-                    f.close()
+-            except IOError:
+-                pass
+-        logger.debug("Writing configuration file %s", file)
+-        logger.debug(output)
+-        return True
+-
+-    @staticmethod
+-    def setOption(name, value):
+-        return {'name': name,
+-                'type': 'option',
+-                'action': 'set',
+-                'value': value}
+-
+-    @staticmethod
+-    def rmOption(name):
+-        return {'name': name,
+-                'type': 'option',
+-                'action': 'remove',
+-                'value': None}
+-
+-    @staticmethod
+-    def setSection(name, options):
+-        return {'name': name,
+-                'type': 'section',
+-                'action': 'set',
+-                'value': options}
+-
+-    @staticmethod
+-    def emptyLine():
+-        return {'name': 'empty',
+-                'type': 'empty'}
++        """something"""
++        warnings.warn(
++            "Use 'ipapython.ipachangeconf.IPAChangeConfg'",
++            DeprecationWarning,
++            stacklevel=2
++        )
++        super(IPAChangeConf, self).__init__(name)
+diff --git a/ipapython/ipachangeconf.py b/ipapython/ipachangeconf.py
+new file mode 100644
+index 0000000..cfb4a6e
+--- /dev/null
++++ b/ipapython/ipachangeconf.py
+@@ -0,0 +1,590 @@
++#
++# ipachangeconf - configuration file manipulation classes and functions
++# partially based on authconfig code
++# Copyright (c) 1999-2007 Red Hat, Inc.
++# Author: Simo Sorce <ssorce@redhat.com>
++#
++# 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/>.
++#
++
++import fcntl
++import logging
++import os
++import shutil
++
++import six
++
++if six.PY3:
++    unicode = str
++
++logger = logging.getLogger(__name__)
++
++
++def openLocked(filename, perms):
++    fd = -1
++    try:
++        fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms)
++
++        fcntl.lockf(fd, fcntl.LOCK_EX)
++    except OSError as e:
++        if fd != -1:
++            try:
++                os.close(fd)
++            except OSError:
++                pass
++        raise IOError(e.errno, e.strerror)
++    return os.fdopen(fd, "r+")
++
++    # TODO: add subsection as a concept
++    #       (ex. REALM.NAME = { foo = x bar = y } )
++    # TODO: put section delimiters as separating element of the list
++    #       so that we can process multiple sections in one go
++    # TODO: add a comment all but provided options as a section option
++
++
++class IPAChangeConf:
++    def __init__(self, name):
++        self.progname = name
++        self.indent = ("", "", "")
++        self.assign = (" = ", "=")
++        self.dassign = self.assign[0]
++        self.comment = ("#",)
++        self.dcomment = self.comment[0]
++        self.eol = ("\n",)
++        self.deol = self.eol[0]
++        self.sectnamdel = ("[", "]")
++        self.subsectdel = ("{", "}")
++        self.case_insensitive_sections = True
++
++    def setProgName(self, name):
++        self.progname = name
++
++    def setIndent(self, indent):
++        if type(indent) is tuple:
++            self.indent = indent
++        elif type(indent) is str:
++            self.indent = (indent, )
++        else:
++            raise ValueError('Indent must be a list of strings')
++
++    def setOptionAssignment(self, assign):
++        if type(assign) is tuple:
++            self.assign = assign
++        else:
++            self.assign = (assign, )
++        self.dassign = self.assign[0]
++
++    def setCommentPrefix(self, comment):
++        if type(comment) is tuple:
++            self.comment = comment
++        else:
++            self.comment = (comment, )
++        self.dcomment = self.comment[0]
++
++    def setEndLine(self, eol):
++        if type(eol) is tuple:
++            self.eol = eol
++        else:
++            self.eol = (eol, )
++        self.deol = self.eol[0]
++
++    def setSectionNameDelimiters(self, delims):
++        self.sectnamdel = delims
++
++    def setSubSectionDelimiters(self, delims):
++        self.subsectdel = delims
++
++    def matchComment(self, line):
++        for v in self.comment:
++            if line.lstrip().startswith(v):
++                return line.lstrip()[len(v):]
++        return False
++
++    def matchEmpty(self, line):
++        if line.strip() == "":
++            return True
++        return False
++
++    def matchSection(self, line):
++        cl = "".join(line.strip().split())
++        cl = cl.lower() if self.case_insensitive_sections else cl
++
++        if len(self.sectnamdel) != 2:
++            return False
++        if not cl.startswith(self.sectnamdel[0]):
++            return False
++        if not cl.endswith(self.sectnamdel[1]):
++            return False
++        return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])]
++
++    def matchSubSection(self, line):
++        if self.matchComment(line):
++            return False
++
++        parts = line.split(self.dassign, 1)
++        if len(parts) < 2:
++            return False
++
++        if parts[1].strip() == self.subsectdel[0]:
++            return parts[0].strip()
++
++        return False
++
++    def matchSubSectionEnd(self, line):
++        if self.matchComment(line):
++            return False
++
++        if line.strip() == self.subsectdel[1]:
++            return True
++
++        return False
++
++    def getSectionLine(self, section):
++        if len(self.sectnamdel) != 2:
++            return section
++        return self._dump_line(self.sectnamdel[0],
++                               section,
++                               self.sectnamdel[1],
++                               self.deol)
++
++    def _dump_line(self, *args):
++        return u"".join(unicode(x) for x in args)
++
++    def dump(self, options, level=0):
++        output = []
++        if level >= len(self.indent):
++            level = len(self.indent) - 1
++
++        for o in options:
++            if o['type'] == "section":
++                output.append(self._dump_line(self.sectnamdel[0],
++                                              o['name'],
++                                              self.sectnamdel[1]))
++                output.append(self.dump(o['value'], (level + 1)))
++                continue
++            if o['type'] == "subsection":
++                output.append(self._dump_line(self.indent[level],
++                                              o['name'],
++                                              self.dassign,
++                                              self.subsectdel[0]))
++                output.append(self.dump(o['value'], (level + 1)))
++                output.append(self._dump_line(self.indent[level],
++                                              self.subsectdel[1]))
++                continue
++            if o['type'] == "option":
++                delim = o.get('delim', self.dassign)
++                if delim not in self.assign:
++                    raise ValueError(
++                        'Unknown delim "%s" must be one of "%s"' %
++                        (delim, " ".join([d for d in self.assign]))
++                    )
++                output.append(self._dump_line(self.indent[level],
++                                              o['name'],
++                                              delim,
++                                              o['value']))
++                continue
++            if o['type'] == "comment":
++                output.append(self._dump_line(self.dcomment, o['value']))
++                continue
++            if o['type'] == "empty":
++                output.append('')
++                continue
++            raise SyntaxError('Unknown type: [%s]' % o['type'])
++
++        # append an empty string to the output so that we add eol to the end
++        # of the file contents in a single join()
++        output.append('')
++        return self.deol.join(output)
++
++    def parseLine(self, line):
++
++        if self.matchEmpty(line):
++            return {'name': 'empty', 'type': 'empty'}
++
++        value = self.matchComment(line)
++        if value:
++            return {'name': 'comment',
++                    'type': 'comment',
++                    'value': value.rstrip()}  # pylint: disable=E1103
++
++        o = dict()
++        parts = line.split(self.dassign, 1)
++        if len(parts) < 2:
++            # The default assign didn't match, try the non-default
++            for d in self.assign[1:]:
++                parts = line.split(d, 1)
++                if len(parts) >= 2:
++                    o['delim'] = d
++                    break
++
++            if 'delim' not in o:
++                raise SyntaxError('Syntax Error: Unknown line format')
++
++        o.update({'name': parts[0].strip(), 'type': 'option',
++                  'value': parts[1].rstrip()})
++        return o
++
++    def findOpts(self, opts, type, name, exclude_sections=False):
++
++        num = 0
++        for o in opts:
++            if o['type'] == type and o['name'] == name:
++                return (num, o)
++            if exclude_sections and (o['type'] == "section" or
++                                     o['type'] == "subsection"):
++                return (num, None)
++            num += 1
++        return (num, None)
++
++    def commentOpts(self, inopts, level=0):
++
++        opts = []
++
++        if level >= len(self.indent):
++            level = len(self.indent) - 1
++
++        for o in inopts:
++            if o['type'] == 'section':
++                no = self.commentOpts(o['value'], (level + 1))
++                val = self._dump_line(self.dcomment,
++                                      self.sectnamdel[0],
++                                      o['name'],
++                                      self.sectnamdel[1])
++                opts.append({'name': 'comment',
++                             'type': 'comment',
++                             'value': val})
++                for n in no:
++                    opts.append(n)
++                continue
++            if o['type'] == 'subsection':
++                no = self.commentOpts(o['value'], (level + 1))
++                val = self._dump_line(self.indent[level],
++                                      o['name'],
++                                      self.dassign,
++                                      self.subsectdel[0])
++                opts.append({'name': 'comment',
++                             'type': 'comment',
++                             'value': val})
++                opts.extend(no)
++                val = self._dump_line(self.indent[level], self.subsectdel[1])
++                opts.append({'name': 'comment',
++                             'type': 'comment',
++                             'value': val})
++                continue
++            if o['type'] == 'option':
++                delim = o.get('delim', self.dassign)
++                if delim not in self.assign:
++                    val = self._dump_line(self.indent[level],
++                                          o['name'],
++                                          delim,
++                                          o['value'])
++                opts.append({'name': 'comment', 'type': 'comment',
++                             'value': val})
++                continue
++            if o['type'] == 'comment':
++                opts.append(o)
++                continue
++            if o['type'] == 'empty':
++                opts.append({'name': 'comment',
++                             'type': 'comment',
++                             'value': ''})
++                continue
++            raise SyntaxError('Unknown type: [%s]' % o['type'])
++
++        return opts
++
++    def mergeOld(self, oldopts, newopts):
++
++        opts = []
++
++        for o in oldopts:
++            if o['type'] == "section" or o['type'] == "subsection":
++                _num, no = self.findOpts(newopts, o['type'], o['name'])
++                if not no:
++                    opts.append(o)
++                    continue
++                if no['action'] == "set":
++                    mo = self.mergeOld(o['value'], no['value'])
++                    opts.append({'name': o['name'],
++                                 'type': o['type'],
++                                 'value': mo})
++                    continue
++                if no['action'] == "comment":
++                    co = self.commentOpts(o['value'])
++                    for c in co:
++                        opts.append(c)
++                    continue
++                if no['action'] == "remove":
++                    continue
++                raise SyntaxError('Unknown action: [%s]' % no['action'])
++
++            if o['type'] == "comment" or o['type'] == "empty":
++                opts.append(o)
++                continue
++
++            if o['type'] == "option":
++                _num, no = self.findOpts(newopts, 'option', o['name'], True)
++                if not no:
++                    opts.append(o)
++                    continue
++                if no['action'] == 'comment' or no['action'] == 'remove':
++                    if (no['value'] is not None and
++                            o['value'] is not no['value']):
++                        opts.append(o)
++                        continue
++                    if no['action'] == 'comment':
++                        value = self._dump_line(self.dcomment,
++                                                o['name'],
++                                                self.dassign,
++                                                o['value'])
++                        opts.append({'name': 'comment',
++                                     'type': 'comment',
++                                     'value': value})
++                    continue
++                if no['action'] == 'set':
++                    opts.append(no)
++                    continue
++                if no['action'] == 'addifnotset':
++                    opts.append({
++                        'name': 'comment',
++                        'type': 'comment',
++                        'value': self._dump_line(
++                            ' ', no['name'], ' modified by IPA'
++                        ),
++                    })
++                    opts.append({'name': 'comment', 'type': 'comment',
++                                'value': self._dump_line(no['name'],
++                                                         self.dassign,
++                                                         no['value'],
++                                                         )})
++                    opts.append(o)
++                    continue
++                raise SyntaxError('Unknown action: [%s]' % no['action'])
++
++            raise SyntaxError('Unknown type: [%s]' % o['type'])
++
++        return opts
++
++    def mergeNew(self, opts, newopts):
++
++        cline = 0
++
++        for no in newopts:
++
++            if no['type'] == "section" or no['type'] == "subsection":
++                (num, o) = self.findOpts(opts, no['type'], no['name'])
++                if not o:
++                    if no['action'] == 'set':
++                        opts.append(no)
++                    continue
++                if no['action'] == "set":
++                    self.mergeNew(o['value'], no['value'])
++                    continue
++                cline = num + 1
++                continue
++
++            if no['type'] == "option":
++                (num, o) = self.findOpts(opts, no['type'], no['name'], True)
++                if not o:
++                    if no['action'] == 'set' or no['action'] == 'addifnotset':
++                        opts.append(no)
++                    continue
++                cline = num + 1
++                continue
++
++            if no['type'] == "comment" or no['type'] == "empty":
++                opts.insert(cline, no)
++                cline += 1
++                continue
++
++            raise SyntaxError('Unknown type: [%s]' % no['type'])
++
++    def merge(self, oldopts, newopts):
++        """
++        Uses a two pass strategy:
++        First we create a new opts tree from oldopts removing/commenting
++          the options as indicated by the contents of newopts
++        Second we fill in the new opts tree with options as indicated
++          in the newopts tree (this is becaus eentire (sub)sections may
++          in the newopts tree (this is becaus entire (sub)sections may
++          exist in the newopts that do not exist in oldopts)
++        """
++        opts = self.mergeOld(oldopts, newopts)
++        self.mergeNew(opts, newopts)
++        return opts
++
++    # TODO: Make parse() recursive?
++    def parse(self, f):
++
++        opts = []
++        sectopts = []
++        section = None
++        subsectopts = []
++        subsection = None
++        curopts = opts
++        fatheropts = opts
++
++        # Read in the old file.
++        for line in f:
++
++            # It's a section start.
++            value = self.matchSection(line)
++            if value:
++                if section is not None:
++                    opts.append({'name': section,
++                                 'type': 'section',
++                                 'value': sectopts})
++                sectopts = []
++                curopts = sectopts
++                fatheropts = sectopts
++                section = value
++                continue
++
++            # It's a subsection start.
++            value = self.matchSubSection(line)
++            if value:
++                if subsection is not None:
++                    raise SyntaxError('nested subsections are not '
++                                      'supported yet')
++                subsectopts = []
++                curopts = subsectopts
++                subsection = value
++                continue
++
++            value = self.matchSubSectionEnd(line)
++            if value:
++                if subsection is None:
++                    raise SyntaxError('Unmatched end subsection terminator '
++                                      'found')
++                fatheropts.append({'name': subsection,
++                                   'type': 'subsection',
++                                   'value': subsectopts})
++                subsection = None
++                curopts = fatheropts
++                continue
++
++            # Copy anything else as is.
++            try:
++                curopts.append(self.parseLine(line))
++            except SyntaxError as e:
++                raise SyntaxError('{error} in file {fname}: [{line}]'.format(
++                    error=e, fname=f.name, line=line.rstrip()))
++
++        # Add last section if any
++        if len(sectopts) is not 0:
++            opts.append({'name': section,
++                         'type': 'section',
++                         'value': sectopts})
++
++        return opts
++
++    def changeConf(self, file, newopts):
++        """
++        Write settings to configuration file
++        :param file: path to the file
++        :param options: set of dictionaries in the form:
++             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
++        :param section: section name like 'global'
++        """
++        output = ""
++        f = None
++        try:
++            # Do not catch an unexisting file error
++            # we want to fail in that case
++            shutil.copy2(file, (file + ".ipabkp"))
++
++            f = openLocked(file, 0o644)
++
++            oldopts = self.parse(f)
++
++            options = self.merge(oldopts, newopts)
++
++            output = self.dump(options)
++
++            # Write it out and close it.
++            f.seek(0)
++            f.truncate(0)
++            f.write(output)
++        finally:
++            try:
++                if f:
++                    f.close()
++            except IOError:
++                pass
++        logger.debug("Updating configuration file %s", file)
++        logger.debug(output)
++        return True
++
++    def newConf(self, file, options, file_perms=0o644):
++        """"
++        Write settings to a new file, backup the old
++        :param file: path to the file
++        :param options: a set of dictionaries in the form:
++             {'name': 'foo', 'value': 'bar', 'action': 'set/comment'}
++        :param file_perms: number defining the new file's permissions
++        """
++        output = ""
++        f = None
++        try:
++            try:
++                shutil.copy2(file, (file + ".ipabkp"))
++            except IOError as err:
++                if err.errno == 2:
++                    # The orign file did not exist
++                    pass
++
++            f = openLocked(file, file_perms)
++
++            # Trunkate
++            f.seek(0)
++            f.truncate(0)
++
++            output = self.dump(options)
++
++            f.write(output)
++        finally:
++            try:
++                if f:
++                    f.close()
++            except IOError:
++                pass
++        logger.debug("Writing configuration file %s", file)
++        logger.debug(output)
++        return True
++
++    @staticmethod
++    def setOption(name, value):
++        return {'name': name,
++                'type': 'option',
++                'action': 'set',
++                'value': value}
++
++    @staticmethod
++    def rmOption(name):
++        return {'name': name,
++                'type': 'option',
++                'action': 'remove',
++                'value': None}
++
++    @staticmethod
++    def setSection(name, options):
++        return {'name': name,
++                'type': 'section',
++                'action': 'set',
++                'value': options}
++
++    @staticmethod
++    def emptyLine():
++        return {'name': 'empty',
++                'type': 'empty'}
+diff --git a/ipaserver/install/adtrustinstance.py b/ipaserver/install/adtrustinstance.py
+index 7bb9431..47a5a92 100644
+--- a/ipaserver/install/adtrustinstance.py
++++ b/ipaserver/install/adtrustinstance.py
+@@ -40,11 +40,11 @@ from ipaserver.install.replication import wait_for_task
+ from ipalib import errors, api
+ from ipalib.util import normalize_zone
+ from ipapython.dn import DN
++from ipapython import ipachangeconf
+ from ipapython import ipaldap
+ from ipapython import ipautil
+ import ipapython.errors
+ 
+-import ipaclient.install.ipachangeconf
+ from ipaplatform import services
+ from ipaplatform.constants import constants
+ from ipaplatform.paths import paths
+@@ -639,7 +639,7 @@ class ADTRUSTInstance(service.Service):
+             self.print_msg("Cannot modify /etc/krb5.conf")
+ 
+         krbconf = (
+-            ipaclient.install.ipachangeconf.IPAChangeConf("IPA Installer"))
++            ipachangeconf.IPAChangeConf("IPA Installer"))
+         krbconf.setOptionAssignment((" = ", " "))
+         krbconf.setSectionNameDelimiters(("[", "]"))
+         krbconf.setSubSectionDelimiters(("{", "}"))
+diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
+index 02c8f4d..6a81d57 100644
+--- a/ipaserver/install/server/install.py
++++ b/ipaserver/install/server/install.py
+@@ -19,7 +19,7 @@ import six
+ from ipaclient.install import timeconf
+ from ipaclient.install.client import (
+     check_ldap_conf, sync_time, restore_time_sync)
+-from ipaclient.install.ipachangeconf import IPAChangeConf
++from ipapython.ipachangeconf import IPAChangeConf
+ from ipalib.install import certmonger, sysrestore
+ from ipapython import ipautil, version
+ from ipapython.ipautil import (
+diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
+index 6da6804..7272640 100644
+--- a/ipaserver/install/server/replicainstall.py
++++ b/ipaserver/install/server/replicainstall.py
+@@ -23,13 +23,13 @@ from pkg_resources import parse_version
+ import six
+ 
+ from ipaclient.install.client import check_ldap_conf
+-from ipaclient.install.ipachangeconf import IPAChangeConf
+ import ipaclient.install.timeconf
+ from ipalib.install import certstore, sysrestore
+ from ipalib.install.kinit import kinit_keytab
+ from ipapython import ipaldap, ipautil
+ from ipapython.dn import DN
+ from ipapython.admintool import ScriptError
++from ipapython.ipachangeconf import IPAChangeConf
+ from ipaplatform import services
+ from ipaplatform.tasks import tasks
+ from ipaplatform.paths import paths
+diff --git a/ipatests/test_install/test_changeconf.py b/ipatests/test_install/test_changeconf.py
+index 2dc2b7d..40c8a1d 100644
+--- a/ipatests/test_install/test_changeconf.py
++++ b/ipatests/test_install/test_changeconf.py
+@@ -3,7 +3,7 @@
+ from __future__ import absolute_import
+ 
+ import pytest
+-from ipaclient.install.ipachangeconf import IPAChangeConf
++from ipapython.ipachangeconf import IPAChangeConf
+ 
+ 
+ @pytest.fixture(scope='function')
+
+From 2da90887632c764a73866c9ad3824ebb53c0aa73 Mon Sep 17 00:00:00 2001
+From: Rob Critenden <rcritten@redhat.com>
+Date: Aug 29 2019 06:45:12 +0000
+Subject: Use tasks to configure automount nsswitch settings
+
+
+authselect doesn't allow one to directly write to
+/etc/nsswitch.conf. It will complain bitterly if it
+detects it and will refuse to work until reset.
+
+Instead it wants the user to write to
+/etc/authselect/user-nsswitch.conf and then it will handle
+merging in any differences.
+
+To complicate matters some databases are not user configurable
+like passwd, group and of course, automount. There are some
+undocumented options to allow one to override these though so
+we utilize that.
+
+tasks are used so that authselect-based installations can still
+write directly to /etc/nsswitch.conf and operate as it used to.
+
+Reviewed-By: Francois Cami <fcami@redhat.com>
+Reviewed-By: Rob Crittenden <rcritten@redhat.com>
+Reviewed-By: Rob Critenden <rcritten@redhat.com>
+Reviewed-By: François Cami <fcami@redhat.com>
+
+---
+
+diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
+index 9492ca4..1e88ba1 100644
+--- a/ipaclient/install/client.py
++++ b/ipaclient/install/client.py
+@@ -66,7 +66,7 @@ from ipapython import version
+ 
+ from . import automount, timeconf, sssd
+ from ipaclient import discovery
+-from .ipachangeconf import IPAChangeConf
++from ipapython.ipachangeconf import IPAChangeConf
+ 
+ NoneType = type(None)
+ 
+@@ -281,72 +281,6 @@ def is_ipa_client_installed(fstore, on_master=False):
+     return installed
+ 
+ 
+-def configure_nsswitch_database(fstore, database, services, preserve=True,
+-                                append=True, default_value=()):
+-    """
+-    Edits the specified nsswitch.conf database (e.g. passwd, group, sudoers)
+-    to use the specified service(s).
+-
+-    Arguments:
+-        fstore - FileStore to backup the nsswitch.conf
+-        database - database configuration that should be ammended,
+-                   e.g. 'sudoers'
+-        service - list of services that should be added, e.g. ['sss']
+-        preserve - if True, the already configured services will be preserved
+-
+-    The next arguments modify the behaviour if preserve=True:
+-        append - if True, the services will be appended, if False, prepended
+-        default_value - list of services that are considered as default (if
+-                        the database is not mentioned in nsswitch.conf), e.g.
+-                        ['files']
+-    """
+-
+-    # Backup the original version of nsswitch.conf, we're going to edit it now
+-    if not fstore.has_file(paths.NSSWITCH_CONF):
+-        fstore.backup_file(paths.NSSWITCH_CONF)
+-
+-    conf = IPAChangeConf("IPA Installer")
+-    conf.setOptionAssignment(':')
+-
+-    if preserve:
+-        # Read the existing configuration
+-        with open(paths.NSSWITCH_CONF, 'r') as f:
+-            opts = conf.parse(f)
+-            raw_database_entry = conf.findOpts(opts, 'option', database)[1]
+-
+-        # Detect the list of already configured services
+-        if not raw_database_entry:
+-            # If there is no database entry, database is not present in
+-            # the nsswitch.conf. Set the list of services to the
+-            # default list, if passed.
+-            configured_services = list(default_value)
+-        else:
+-            configured_services = raw_database_entry['value'].strip().split()
+-
+-        # Make sure no service is added if already mentioned in the list
+-        added_services = [s for s in services
+-                          if s not in configured_services]
+-
+-        # Prepend / append the list of new services
+-        if append:
+-            new_value = ' ' + ' '.join(configured_services + added_services)
+-        else:
+-            new_value = ' ' + ' '.join(added_services + configured_services)
+-
+-    else:
+-        # Preserve not set, let's rewrite existing configuration
+-        new_value = ' ' + ' '.join(services)
+-
+-    # Set new services as sources for database
+-    opts = [
+-        conf.setOption(database, new_value),
+-        conf.emptyLine(),
+-    ]
+-
+-    conf.changeConf(paths.NSSWITCH_CONF, opts)
+-    logger.info("Configured %s in %s", database, paths.NSSWITCH_CONF)
+-
+-
+ def configure_ipa_conf(
+         fstore, cli_basedn, cli_realm, cli_domain, cli_server, hostname):
+     ipaconf = IPAChangeConf("IPA Installer")
+@@ -948,9 +882,7 @@ def configure_sssd_conf(
+                 "Unable to activate the SUDO service in SSSD config.")
+ 
+         sssdconfig.activate_service('sudo')
+-        configure_nsswitch_database(
+-            fstore, 'sudoers', ['sss'],
+-            default_value=['files'])
++        tasks.enable_sssd_sudo(fstore)
+ 
+     domain.add_provider('ipa', 'id')
+ 
+diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py
+index a1dc2a1..3a0896b 100644
+--- a/ipaclient/install/ipa_client_automount.py
++++ b/ipaclient/install/ipa_client_automount.py
+@@ -41,7 +41,8 @@ from six.moves.urllib.parse import urlsplit
+ 
+ # pylint: enable=import-error
+ from optparse import OptionParser  # pylint: disable=deprecated-module
+-from ipaclient.install import ipachangeconf, ipadiscovery
++from ipapython import ipachangeconf
++from ipaclient.install import ipadiscovery
+ from ipaclient.install.client import (
+     CLIENT_NOT_CONFIGURED,
+     CLIENT_ALREADY_CONFIGURED,
+@@ -177,44 +178,6 @@ def configure_xml(fstore):
+         print("Configured %s" % authconf)
+ 
+ 
+-def configure_nsswitch(statestore, options):
+-    """
+-    Point automount to ldap in nsswitch.conf.
+-    This function is for non-SSSD setups only.
+-    """
+-    conf = ipachangeconf.IPAChangeConf("IPA Installer")
+-    conf.setOptionAssignment(':')
+-
+-    with open(paths.NSSWITCH_CONF, 'r') as f:
+-        current_opts = conf.parse(f)
+-        current_nss_value = conf.findOpts(
+-            current_opts, name='automount', type='option'
+-        )[1]
+-        if current_nss_value is None:
+-            # no automount database present
+-            current_nss_value = False  # None cannot be backed up
+-        else:
+-            current_nss_value = current_nss_value['value']
+-        statestore.backup_state(
+-            'ipa-client-automount-nsswitch', 'previous-automount',
+-            current_nss_value
+-        )
+-
+-    nss_value = ' files ldap'
+-    opts = [
+-        {
+-            'name': 'automount',
+-            'type': 'option',
+-            'action': 'set',
+-            'value': nss_value,
+-        },
+-        {'name': 'empty', 'type': 'empty'},
+-    ]
+-    conf.changeConf(paths.NSSWITCH_CONF, opts)
+-
+-    print("Configured %s" % paths.NSSWITCH_CONF)
+-
+-
+ def configure_autofs_sssd(fstore, statestore, autodiscover, options):
+     try:
+         sssdconfig = SSSDConfig.SSSDConfig()
+@@ -339,41 +302,8 @@ def uninstall(fstore, statestore):
+     ]
+     STATES = ['autofs', 'rpcidmapd', 'rpcgssd']
+ 
+-    if statestore.get_state(
+-        'ipa-client-automount-nsswitch', 'previous-automount'
+-    ) is False:
+-        # Previous nsswitch.conf had no automount database configured
+-        # so remove it.
+-        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
+-        conf.setOptionAssignment(':')
+-        changes = [conf.rmOption('automount')]
+-        conf.changeConf(paths.NSSWITCH_CONF, changes)
+-        tasks.restore_context(paths.NSSWITCH_CONF)
+-        statestore.delete_state(
+-            'ipa-client-automount-nsswitch', 'previous-automount'
+-        )
+-    elif statestore.get_state(
+-        'ipa-client-automount-nsswitch', 'previous-automount'
+-    ) is not None:
+-        nss_value = statestore.get_state(
+-            'ipa-client-automount-nsswitch', 'previous-automount'
+-        )
+-        opts = [
+-            {
+-                'name': 'automount',
+-                'type': 'option',
+-                'action': 'set',
+-                'value': nss_value,
+-            },
+-            {'name': 'empty', 'type': 'empty'},
+-        ]
+-        conf = ipachangeconf.IPAChangeConf("IPA automount installer")
+-        conf.setOptionAssignment(':')
+-        conf.changeConf(paths.NSSWITCH_CONF, opts)
+-        tasks.restore_context(paths.NSSWITCH_CONF)
+-        statestore.delete_state(
+-            'ipa-client-automount-nsswitch', 'previous-automount'
+-        )
++    if not statestore.get_state('autofs', 'sssd'):
++        tasks.disable_ldap_automount(statestore)
+ 
+     if not any(fstore.has_file(f) for f in RESTORE_FILES) or not any(
+         statestore.has_state(s) for s in STATES
+@@ -627,7 +557,7 @@ def configure_automount():
+ 
+     try:
+         if not options.sssd:
+-            configure_nsswitch(statestore, options)
++            tasks.enable_ldap_automount(statestore)
+         configure_nfs(fstore, statestore, options)
+         if options.sssd:
+             configure_autofs_sssd(fstore, statestore, autodiscover, options)
+diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py
+index 8aa9c5c..7fd7d57 100644
+--- a/ipaplatform/base/tasks.py
++++ b/ipaplatform/base/tasks.py
+@@ -32,6 +32,7 @@ from pkg_resources import parse_version
+ from ipaplatform.constants import constants
+ from ipaplatform.paths import paths
+ from ipapython import ipautil
++from ipapython.ipachangeconf import IPAChangeConf
+ 
+ logger = logging.getLogger(__name__)
+ 
+@@ -337,5 +338,157 @@ class BaseTaskNamespace:
+         """
+         raise NotImplementedError
+ 
++    def configure_nsswitch_database(self, fstore, database, services,
++                                    preserve=True, append=True,
++                                    default_value=()):
++        """
++        Edits the specified nsswitch.conf database (e.g. passwd, group,
++        sudoers) to use the specified service(s).
++
++        Arguments:
++            fstore - FileStore to backup the nsswitch.conf
++            database - database configuration that should be ammended,
++                       e.g. 'sudoers'
++            service - list of services that should be added, e.g. ['sss']
++            preserve - if True, the already configured services will be
++                       preserved
++
++        The next arguments modify the behaviour if preserve=True:
++            append - if True, the services will be appended, if False,
++                     prepended
++            default_value - list of services that are considered as default (if
++                            the database is not mentioned in nsswitch.conf),
++                            e.g. ['files']
++        """
++
++        # Backup the original version of nsswitch.conf, we're going to edit it
++        # now
++        if not fstore.has_file(paths.NSSWITCH_CONF):
++            fstore.backup_file(paths.NSSWITCH_CONF)
++
++        conf = IPAChangeConf("IPA Installer")
++        conf.setOptionAssignment(':')
++
++        if preserve:
++            # Read the existing configuration
++            with open(paths.NSSWITCH_CONF, 'r') as f:
++                opts = conf.parse(f)
++                raw_database_entry = conf.findOpts(opts, 'option', database)[1]
++
++            # Detect the list of already configured services
++            if not raw_database_entry:
++                # If there is no database entry, database is not present in
++                # the nsswitch.conf. Set the list of services to the
++                # default list, if passed.
++                configured_services = list(default_value)
++            else:
++                configured_services = raw_database_entry[
++                    'value'].strip().split()
++
++            # Make sure no service is added if already mentioned in the list
++            added_services = [s for s in services
++                              if s not in configured_services]
++
++            # Prepend / append the list of new services
++            if append:
++                new_value = ' ' + ' '.join(configured_services +
++                                           added_services)
++            else:
++                new_value = ' ' + ' '.join(added_services +
++                                           configured_services)
++
++        else:
++            # Preserve not set, let's rewrite existing configuration
++            new_value = ' ' + ' '.join(services)
++
++        # Set new services as sources for database
++        opts = [
++            conf.setOption(database, new_value),
++            conf.emptyLine(),
++        ]
++
++        conf.changeConf(paths.NSSWITCH_CONF, opts)
++        logger.info("Configured %s in %s", database, paths.NSSWITCH_CONF)
++
++    def enable_sssd_sudo(self, fstore):
++        """Configure nsswitch.conf to use sssd for sudo"""
++        self.configure_nsswitch_database(
++            fstore, 'sudoers', ['sss'],
++            default_value=['files'])
++
++    def enable_ldap_automount(self, statestore):
++        """
++        Point automount to ldap in nsswitch.conf.
++        This function is for non-SSSD setups only.
++        """
++        conf = IPAChangeConf("IPA Installer")
++        conf.setOptionAssignment(':')
++
++        with open(paths.NSSWITCH_CONF, 'r') as f:
++            current_opts = conf.parse(f)
++            current_nss_value = conf.findOpts(
++                current_opts, name='automount', type='option'
++            )[1]
++            if current_nss_value is None:
++                # no automount database present
++                current_nss_value = False  # None cannot be backed up
++            else:
++                current_nss_value = current_nss_value['value']
++            statestore.backup_state(
++                'ipa-client-automount-nsswitch', 'previous-automount',
++                current_nss_value
++            )
++
++        nss_value = ' files ldap'
++        opts = [
++            {
++                'name': 'automount',
++                'type': 'option',
++                'action': 'set',
++                'value': nss_value,
++            },
++            {'name': 'empty', 'type': 'empty'},
++        ]
++        conf.changeConf(paths.NSSWITCH_CONF, opts)
++
++        logger.info("Configured %s", paths.NSSWITCH_CONF)
++
++    def disable_ldap_automount(self, statestore):
++        """Disable automount using LDAP"""
++        if statestore.get_state(
++            'ipa-client-automount-nsswitch', 'previous-automount'
++        ) is False:
++            # Previous nsswitch.conf had no automount database configured
++            # so remove it.
++            conf = IPAChangeConf("IPA automount installer")
++            conf.setOptionAssignment(':')
++            changes = [conf.rmOption('automount')]
++            conf.changeConf(paths.NSSWITCH_CONF, changes)
++            self.restore_context(paths.NSSWITCH_CONF)
++            statestore.delete_state(
++                'ipa-client-automount-nsswitch', 'previous-automount'
++            )
++        elif statestore.get_state(
++            'ipa-client-automount-nsswitch', 'previous-automount'
++        ) is not None:
++            nss_value = statestore.get_state(
++                'ipa-client-automount-nsswitch', 'previous-automount'
++            )
++            opts = [
++                {
++                    'name': 'automount',
++                    'type': 'option',
++                    'action': 'set',
++                    'value': nss_value,
++                },
++                {'name': 'empty', 'type': 'empty'},
++            ]
++            conf = IPAChangeConf("IPA automount installer")
++            conf.setOptionAssignment(':')
++            conf.changeConf(paths.NSSWITCH_CONF, opts)
++            self.restore_context(paths.NSSWITCH_CONF)
++            statestore.delete_state(
++                'ipa-client-automount-nsswitch', 'previous-automount'
++            )
+ 
+ tasks = BaseTaskNamespace()
+diff --git a/ipaplatform/redhat/paths.py b/ipaplatform/redhat/paths.py
+index 8ccd04b..15bdef6 100644
+--- a/ipaplatform/redhat/paths.py
++++ b/ipaplatform/redhat/paths.py
+@@ -39,6 +39,7 @@ class RedHatPathNamespace(BasePathNamespace):
+     AUTHCONFIG = '/usr/sbin/authconfig'
+     AUTHSELECT = '/usr/bin/authselect'
+     SYSCONF_NETWORK = '/etc/sysconfig/network'
++    NSSWITCH_CONF = '/etc/authselect/user-nsswitch.conf'
+ 
+ 
+ paths = RedHatPathNamespace()
+diff --git a/ipaplatform/redhat/tasks.py b/ipaplatform/redhat/tasks.py
+index be0b641..e18f6fa 100644
+--- a/ipaplatform/redhat/tasks.py
++++ b/ipaplatform/redhat/tasks.py
+@@ -744,4 +744,23 @@ class RedHatTaskNamespace(BaseTaskNamespace):
+ 
+         return filenames
+ 
++    def enable_ldap_automount(self, statestore):
++        """
++        Point automount to ldap in nsswitch.conf.
++        This function is for non-SSSD setups only.
++        """
++        super(RedHatTaskNamespace, self).enable_ldap_automount(statestore)
++
++        authselect_cmd = [paths.AUTHSELECT, "enable-feature",
++                          "with-custom-automount"]
++        ipautil.run(authselect_cmd)
++
++    def disable_ldap_automount(self, statestore):
++        """Disable ldap-based automount"""
++        super(RedHatTaskNamespace, self).disable_ldap_automount(statestore)
++
++        authselect_cmd = [paths.AUTHSELECT, "disable-feature",
++                          "with-custom-automount"]
++        ipautil.run(authselect_cmd)
++
+ tasks = RedHatTaskNamespace()
+
diff --git a/SOURCES/0010-Handle_NTP_configuration_in_replica_server_installation_f3e3da5_rhbz#1651679.patch b/SOURCES/0010-Handle_NTP_configuration_in_replica_server_installation_f3e3da5_rhbz#1651679.patch
deleted file mode 100644
index b5d4f50..0000000
--- a/SOURCES/0010-Handle_NTP_configuration_in_replica_server_installation_f3e3da5_rhbz#1651679.patch
+++ /dev/null
@@ -1,69 +0,0 @@
-From f3e3da509329881c4ba770d1f9418ad180ee98ae Mon Sep 17 00:00:00 2001
-From: Rob Crittenden <rcritten@redhat.com>
-Date: Oct 19 2018 17:35:05 +0000
-Subject: Handle NTP configuration in a replica server installation
-
-
-There were two separate issues:
-
-1. If not enrolling on a pre-configured client then the ntp-server and
-   ntp-pool options are not being passed down to the client installer
-   invocation.
-2. If the client is already enrolled then the ntp options are ignored
-   altogether.
-
-In the first case simply pass down the options to the client
-installer invocation.
-
-If the client is pre-enrolled and NTP options are provided then
-raise an exception.
-
-https://pagure.io/freeipa/issue/7723
-
-Signed-off-by: Rob Crittenden <rcritten@redhat.com>
-Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
-
----
-
-diff --git a/install/tools/man/ipa-replica-install.1 b/install/tools/man/ipa-replica-install.1
-index 7f6ca57..c63107d 100644
---- a/install/tools/man/ipa-replica-install.1
-+++ b/install/tools/man/ipa-replica-install.1
-@@ -14,7 +14,7 @@ Domain level 0 is not supported anymore.
- 
- To create a replica, the machine only needs to be enrolled in the FreeIPA domain first. This process of turning the IPA client into a replica is also referred to as replica promotion.
- 
--If you're starting with an existing IPA client, simply run ipa\-replica\-install to have it promoted into a replica.
-+If you're starting with an existing IPA client, simply run ipa\-replica\-install to have it promoted into a replica. The NTP configuration cannot be updated during client promotion. 
- 
- To promote a blank machine into a replica, you have two options, you can either run ipa\-client\-install in a separate step, or pass the enrollment related options to the ipa\-replica\-install (see CLIENT ENROLLMENT OPTIONS). In the latter case, ipa\-replica\-install will join the machine to the IPA realm automatically and will proceed with the promotion step.
- 
-diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
-index aaa1943..3022057 100644
---- a/ipaserver/install/server/replicainstall.py
-+++ b/ipaserver/install/server/replicainstall.py
-@@ -717,6 +717,11 @@ def ensure_enrolled(installer):
-         for ip in installer.ip_addresses:
-             # installer.ip_addresses is of type [CheckedIPAddress]
-             args.extend(("--ip-address", str(ip)))
-+    if installer.ntp_servers:
-+        for server in installer.ntp_servers:
-+            args.extend(("--ntp-server", server))
-+    if installer.ntp_pool:
-+        args.extend(("--ntp-pool", installer.ntp_pool))
- 
-     try:
-         # Call client install script
-@@ -774,6 +779,11 @@ def promote_check(installer):
-                   "the --domain, --server, --realm, --hostname, --password "
-                   "and --keytab options.")
- 
-+        # The NTP configuration can not be touched on pre-installed client:
-+        if options.no_ntp or options.ntp_servers or options.ntp_pool:
-+                raise ScriptError(
-+                    "NTP configuration cannot be updated during promotion")
-+
-     sstore = sysrestore.StateFile(paths.SYSRESTORE)
- 
-     fstore = sysrestore.FileStore(paths.SYSRESTORE)
-
diff --git a/SOURCES/0011-Fix_defects_found_by_static_analysis_rhbz#1658182.patch b/SOURCES/0011-Fix_defects_found_by_static_analysis_rhbz#1658182.patch
deleted file mode 100644
index 539a57c..0000000
--- a/SOURCES/0011-Fix_defects_found_by_static_analysis_rhbz#1658182.patch
+++ /dev/null
@@ -1,449 +0,0 @@
-From 705e280eafb13b1b55fc0b91001e4721ce79fbdf Mon Sep 17 00:00:00 2001
-From: Thomas Woerner <twoerner@redhat.com>
-Date: Mon, 22 Oct 2018 13:57:11 +0200
-Subject: [PATCH] Fix ressource leak in client/config.c get_config_entry
-
-The leak happens due to using strndup to create a temporary string without
-freeing it afterwards.
-
-See: https://pagure.io/freeipa/issue/7738
-Signed-off-by: Thomas Woerner <twoerner@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- client/config.c | 3 ++-
- 1 file changed, 2 insertions(+), 1 deletion(-)
-
-diff --git a/client/config.c b/client/config.c
-index ecc126ff47..a09564b702 100644
---- a/client/config.c
-+++ b/client/config.c
-@@ -123,17 +123,18 @@ get_config_entry(char * in_data, const char *section, const char *key)
-             line++;
-             p = strchr(line, ']');
-             if (p) {
--                tmp = strndup(line, p - line);
-                 if (in_section) {
-                     /* We exited the matching section without a match */
-                     free(data);
-                     return NULL;
-                 }
-+                tmp = strndup(line, p - line);
-                 if (strcmp(section, tmp) == 0) {
-                     free(tmp);
-                     in_section = 1;
-                     continue;
-                 }
-+                free(tmp);
-             }
-         } /* [ */
- 
-From ebb14ed6f57c5504dc2f44339274b108483efd16 Mon Sep 17 00:00:00 2001
-From: Thomas Woerner <twoerner@redhat.com>
-Date: Mon, 22 Oct 2018 15:18:23 +0200
-Subject: [PATCH] Fix ressource leak in
- daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c ipa_cldap_netlogon
-
-The leak happens due to using strndup in a for loop to create a temporary
-string without freeing it in all cases.
-
-See: https://pagure.io/freeipa/issue/7738
-Signed-off-by: Thomas Woerner <twoerner@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c | 4 ++++
- 1 file changed, 4 insertions(+)
-
-diff --git a/daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c b/daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c
-index 5863f667ea..460f96cd59 100644
---- a/daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c
-+++ b/daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c
-@@ -260,6 +260,10 @@ int ipa_cldap_netlogon(struct ipa_cldap_ctx *ctx,
-             if (req->kvps.pairs[i].value.bv_val[len-1] == '.') {
-                 len--;
-             }
-+            if (domain != NULL) {
-+                free(domain);
-+                domain = NULL;
-+            }
-             domain = strndup(req->kvps.pairs[i].value.bv_val, len);
-             if (!domain) {
-                 ret = ENOMEM;
-From 305150416429b85d46ad4162bac492db303cf9cf Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Wed, 24 Oct 2018 10:12:39 +0200
-Subject: [PATCH] Fix ipadb_multires resource handling
-
-* ipadb_get_pwd_policy() initializes struct ipadb_multires *res to NULL.
-* ipadb_multires_free() supports NULL as no-op.
-* ipadb_multibase_search() consistently frees and NULLs
-  struct ipadb_multires **res on error.
-
-See: https://pagure.io/freeipa/issue/7738
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- daemons/ipa-kdb/ipa_kdb_common.c    | 13 +++++++++----
- daemons/ipa-kdb/ipa_kdb_pwdpolicy.c |  2 +-
- 2 files changed, 10 insertions(+), 5 deletions(-)
-
-diff --git a/daemons/ipa-kdb/ipa_kdb_common.c b/daemons/ipa-kdb/ipa_kdb_common.c
-index 5995efe6b1..e2592cea3f 100644
---- a/daemons/ipa-kdb/ipa_kdb_common.c
-+++ b/daemons/ipa-kdb/ipa_kdb_common.c
-@@ -634,10 +634,12 @@ krb5_error_code ipadb_multires_init(LDAP *lcontext, struct ipadb_multires **r)
- 
- void ipadb_multires_free(struct ipadb_multires *r)
- {
--    for (int i = 0; i < r->count; i++) {
--        ldap_msgfree(r->res[i]);
-+    if (r != NULL) {
-+        for (int i = 0; i < r->count; i++) {
-+            ldap_msgfree(r->res[i]);
-+        }
-+        free(r);
-     }
--    free(r);
- }
- 
- LDAPMessage *ipadb_multires_next_entry(struct ipadb_multires *r)
-@@ -670,8 +672,11 @@ krb5_error_code ipadb_multibase_search(struct ipadb_context *ipactx,
-     if (ret != 0) return ret;
- 
-     ret = ipadb_check_connection(ipactx);
--    if (ret != 0)
-+    if (ret != 0) {
-+        ipadb_multires_free(*res);
-+        *res = NULL;
-         return ipadb_simple_ldap_to_kerr(ret);
-+    }
- 
-     for (int b = 0; basedns[b]; b++) {
-         LDAPMessage *r;
-diff --git a/daemons/ipa-kdb/ipa_kdb_pwdpolicy.c b/daemons/ipa-kdb/ipa_kdb_pwdpolicy.c
-index 1ec584612b..10f128700b 100644
---- a/daemons/ipa-kdb/ipa_kdb_pwdpolicy.c
-+++ b/daemons/ipa-kdb/ipa_kdb_pwdpolicy.c
-@@ -141,7 +141,7 @@ krb5_error_code ipadb_get_pwd_policy(krb5_context kcontext, char *name,
-     char *esc_name = NULL;
-     char *src_filter = NULL;
-     krb5_error_code kerr;
--    struct ipadb_multires *res;
-+    struct ipadb_multires *res = NULL;
-     LDAPMessage *lentry;
-     osa_policy_ent_t pentry = NULL;
-     uint32_t result;
-From 4ca3120b9a09ad48866446af29b38ca7c005b0d0 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Wed, 24 Oct 2018 10:19:14 +0200
-Subject: [PATCH] Don't abuse strncpy() length limitation
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-On two occasions C code abused strncpy()'s length limitation to copy a
-string of known length without the trailing NULL byte. Recent GCC is
-raising the compiler warning:
-
-  warning: ‘strncpy’ output truncated before terminating nul copying as
-  many bytes from a string as its length [-Wstringop-truncation]
-
-Use memcpy() instead if strncpy() to copy data of known size.
-
-See: https://pagure.io/freeipa/issue/7738
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- daemons/ipa-kdb/ipa_kdb.c                        | 2 +-
- daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c | 2 +-
- 2 files changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/daemons/ipa-kdb/ipa_kdb.c b/daemons/ipa-kdb/ipa_kdb.c
-index 00c732624b..20967316ed 100644
---- a/daemons/ipa-kdb/ipa_kdb.c
-+++ b/daemons/ipa-kdb/ipa_kdb.c
-@@ -110,7 +110,7 @@ static char *ipadb_realm_to_ldapi_uri(char *realm)
-     /* copy path and escape '/' to '%2f' */
-     for (q = LDAPIDIR; *q; q++) {
-         if (*q == '/') {
--            strncpy(p, "%2f", 3);
-+            memcpy(p, "%2f", 3);
-             p += 3;
-         } else {
-             *p = *q;
-diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c
-index db7183bf2b..61b46904ab 100644
---- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c
-+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c
-@@ -1003,7 +1003,7 @@ int ipapwd_set_extradata(const char *dn,
-     xdata[5] = (unixtime & 0xff000000) >> 24;
- 
-     /* append the principal name */
--    strncpy(&xdata[6], principal, p_len);
-+    memcpy(&xdata[6], principal, p_len);
- 
-     xdata[xd_len -1] = 0;
- 
-From a06fb8d0f7b7c6aba942186b93d87823398f5337 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Thu, 1 Nov 2018 11:41:29 +0100
-Subject: [PATCH] has_krbprincipalkey: avoid double free
-
-Set keys to NULL after free rder to avoid potential double free.
-
-See: https://pagure.io/freeipa/issue/7738
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c | 6 +++++-
- 1 file changed, 5 insertions(+), 1 deletion(-)
-
-diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
-index 209d596255..3c3c7e8845 100644
---- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
-+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
-@@ -176,7 +176,11 @@ static bool has_krbprincipalkey(Slapi_Entry *entry) {
- 
-             if (rc || (num_keys <= 0)) {
-                 /* this one is not valid, ignore it */
--                if (keys) ipa_krb5_free_key_data(keys, num_keys);
-+                if (keys) {
-+                    ipa_krb5_free_key_data(keys, num_keys);
-+                    keys = NULL;
-+                    num_keys = 0;
-+                }
-             } else {
-                 /* It exists at least this one that is valid, no need to continue */
-                 if (keys) ipa_krb5_free_key_data(keys, num_keys);
-From 2884ab69babfd7d40f951ba814234ce4763b0cd8 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Thu, 1 Nov 2018 11:41:41 +0100
-Subject: [PATCH] ipadb_mspac_get_trusted_domains: NULL ptr deref
-
-Fix potential NULL pointer deref in ipadb_mspac_get_trusted_domains().
-In theory, dn could be empty and rdn NULL. The man page for ldap_str2dn()
-does not guarantee that it returns a non-empty result.
-
-See: https://pagure.io/freeipa/issue/7738
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- daemons/ipa-kdb/ipa_kdb_mspac.c | 6 ++++++
- 1 file changed, 6 insertions(+)
-
-diff --git a/daemons/ipa-kdb/ipa_kdb_mspac.c b/daemons/ipa-kdb/ipa_kdb_mspac.c
-index 11e036986a..329a5c1158 100644
---- a/daemons/ipa-kdb/ipa_kdb_mspac.c
-+++ b/daemons/ipa-kdb/ipa_kdb_mspac.c
-@@ -2586,6 +2586,12 @@ krb5_error_code ipadb_mspac_get_trusted_domains(struct ipadb_context *ipactx)
-         }
- 
-         /* We should have a single AVA in the domain RDN */
-+        if (rdn == NULL) {
-+            ldap_dnfree(dn);
-+            ret = EINVAL;
-+            goto done;
-+        }
-+
-         t[n].parent_name = strndup(rdn[0]->la_value.bv_val, rdn[0]->la_value.bv_len);
- 
-         ldap_dnfree(dn);
-From 28b89df5ed8a9a060227433e8eeebf7eea844bb9 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Thu, 1 Nov 2018 11:41:47 +0100
-Subject: [PATCH] ipapwd_pre_mod: NULL ptr deref
-
-In ipapwd_pre_mod, check userpw for NULL before dereferencing its first
-element.
-
-See: https://pagure.io/freeipa/issue/7738
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
-index 3c3c7e8845..9aef2f7d7d 100644
---- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
-+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
-@@ -766,7 +766,7 @@ static int ipapwd_pre_mod(Slapi_PBlock *pb)
-     /* Check this is a clear text password, or refuse operation (only if we need
-      * to comput other hashes */
-     if (! unhashedpw && (gen_krb_keys || is_smb || is_ipant)) {
--        if ('{' == userpw[0]) {
-+        if ((userpw != NULL) && ('{' == userpw[0])) {
-             if (0 == strncasecmp(userpw, "{CLEAR}", strlen("{CLEAR}"))) {
-                 unhashedpw = slapi_ch_strdup(&userpw[strlen("{CLEAR}")]);
-                 if (NULL == unhashedpw) {
-From 5abe3d9feff3c0e66a43fa3799611521f83ee893 Mon Sep 17 00:00:00 2001
-From: Alexander Bokovoy <abokovoy@redhat.com>
-Date: Wed, 7 Nov 2018 11:57:53 +0200
-Subject: [PATCH] ipaserver.install.adtrust: fix CID 323644
-
-Fix Coverity finding CID 323644: logically dead code path
-
-The code to determine whether NetBIOS name was already set or need to be
-set after deriving it from a domain or asking a user for an interactive
-input, was refactored at some point to avoid retrieving the whole LDAP
-entry. Instead, it was provided with the actual NetBIOS name retrieved.
-
-As result, a part of the code got neglected and was never executed.
-
-Fix this code and provide a test that tries to test predefined,
-interactively provided and automatically derived NetBIOS name depending
-on how the installer is being run.
-
-We mock up the actual execution so that no access to LDAP or Samba is
-needed.
-
-Backport to ipa-4-7 takes into account Python 2.7 differences:
- - uses mock instead of unittest.mock if the latter is not available
- - derives ApiMockup from object
-
-Fixes: https://pagure.io/freeipa/issue/7753
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-(cherry picked from commit 82af034023b03ae64f005c8160b9e961e7b9fd55)
-
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- ipaserver/install/adtrust.py                  |  3 +-
- .../test_ipaserver/test_adtrust_mockup.py     | 58 +++++++++++++++++++
- 2 files changed, 59 insertions(+), 2 deletions(-)
- create mode 100644 ipatests/test_ipaserver/test_adtrust_mockup.py
-
-diff --git a/ipaserver/install/adtrust.py b/ipaserver/install/adtrust.py
-index e9ae3fa3ed..75194eed8f 100644
---- a/ipaserver/install/adtrust.py
-+++ b/ipaserver/install/adtrust.py
-@@ -95,7 +95,6 @@ def set_and_check_netbios_name(netbios_name, unattended, api):
-     cur_netbios_name = None
-     gen_netbios_name = None
-     reset_netbios_name = False
--    entry = None
- 
-     if api.Backend.ldap2.isconnected():
-         cur_netbios_name = retrieve_netbios_name(api)
-@@ -133,7 +132,7 @@ def set_and_check_netbios_name(netbios_name, unattended, api):
-             gen_netbios_name = adtrustinstance.make_netbios_name(
-                 api.env.domain)
- 
--        if entry is not None:
-+        if gen_netbios_name is not None:
-             # Fix existing trust configuration
-             print("Trust is configured but no NetBIOS domain name found, "
-                   "setting it now.")
-diff --git a/ipatests/test_ipaserver/test_adtrust_mockup.py b/ipatests/test_ipaserver/test_adtrust_mockup.py
-new file mode 100644
-index 0000000000..614a06f8c8
---- /dev/null
-+++ b/ipatests/test_ipaserver/test_adtrust_mockup.py
-@@ -0,0 +1,58 @@
-+# Copyright (C) 2018  FreeIPA Project Contributors - see LICENSE file
-+
-+from __future__ import print_function
-+import ipaserver.install.adtrust as adtr
-+from ipaserver.install.adtrust import set_and_check_netbios_name
-+from collections import namedtuple
-+from unittest import TestCase
-+try:
-+    from unittest import mock
-+except ImportError:
-+    import mock
-+from io import StringIO
-+
-+
-+class ApiMockup(object):
-+    Backend = namedtuple('Backend', 'ldap2')
-+    Calls = namedtuple('Callbacks', 'retrieve_netbios_name')
-+    env = namedtuple('Environment', 'domain')
-+
-+
-+class TestNetbiosName(TestCase):
-+    @classmethod
-+    def setUpClass(cls):
-+        api = ApiMockup()
-+        ldap2 = namedtuple('LDAP', 'isconnected')
-+        ldap2.isconnected = mock.MagicMock(return_value=True)
-+        api.Backend.ldap2 = ldap2
-+        api.Calls.retrieve_netbios_name = adtr.retrieve_netbios_name
-+        adtr.retrieve_netbios_name = mock.MagicMock(return_value=None)
-+        cls.api = api
-+
-+    @classmethod
-+    def tearDownClass(cls):
-+        adtr.retrieve_netbios_name = cls.api.Calls.retrieve_netbios_name
-+
-+    def test_NetbiosName(self):
-+        """
-+        Test set_and_check_netbios_name() using permutation of two inputs:
-+        - predefined and not defined NetBIOS name
-+        - unattended and interactive run
-+        As result, the function has to return expected NetBIOS name in
-+        all cases. For interactive run we override input to force what
-+        we expect.
-+        """
-+        self.api.env.domain = 'example.com'
-+        expected_nname = 'EXAMPLE'
-+        # NetBIOS name, unattended, should set the name?
-+        tests = ((expected_nname, True, False),
-+                 (None, True, True),
-+                 (None, False, True),
-+                 (expected_nname, False, False))
-+        with mock.patch('sys.stdin', new_callable=StringIO) as stdin:
-+            stdin.write(expected_nname + '\r')
-+            for test in tests:
-+                nname, setname = set_and_check_netbios_name(
-+                    test[0], test[1], self.api)
-+                assert expected_nname == nname
-+                assert setname == test[2]
-From 48a6048be2a3c6cf496a67a2732b8aaee91af620 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Thu, 8 Nov 2018 10:42:43 +0100
-Subject: [PATCH] Copy-paste error in permssions plugin, CID 323649
-
-Address a bug in the code block for attributeLevelRights for old clients.
-The backward compatibility code for deprecated options was not triggered,
-because the new name was checked against wrong dict.
-
-Coverity Scan issue 323649, Copy-paste error
-
-   The copied code will not have its intended effect.
-   In postprocess_result: A copied piece of code is inconsistent with the
-   original (CWE-398)
-
-See: Fixes: https://pagure.io/freeipa/issue/7753
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- ipaserver/plugins/permission.py                    | 2 +-
- ipatests/test_xmlrpc/test_old_permission_plugin.py | 4 ++--
- 2 files changed, 3 insertions(+), 3 deletions(-)
-
-diff --git a/ipaserver/plugins/permission.py b/ipaserver/plugins/permission.py
-index 2127d8234e..8ffe01bd88 100644
---- a/ipaserver/plugins/permission.py
-+++ b/ipaserver/plugins/permission.py
-@@ -486,7 +486,7 @@ def postprocess_result(self, entry, options):
- 
-             if old_client:
-                 for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items():
--                    if new_name in entry:
-+                    if new_name in rights:
-                         rights[old_name] = rights[new_name]
-                         del rights[new_name]
- 
-diff --git a/ipatests/test_xmlrpc/test_old_permission_plugin.py b/ipatests/test_xmlrpc/test_old_permission_plugin.py
-index 6d1117b6b3..600e449421 100644
---- a/ipatests/test_xmlrpc/test_old_permission_plugin.py
-+++ b/ipatests/test_xmlrpc/test_old_permission_plugin.py
-@@ -73,8 +73,8 @@
-                                      'ipapermbindruletype': u'rscwo',
-                                      'ipapermdefaultattr': u'rscwo',
-                                      'ipapermexcludedattr': u'rscwo',
--                                     'ipapermlocation': u'rscwo',
--                                     'ipapermright': u'rscwo',
-+                                     'subtree': u'rscwo',  # old
-+                                     'permissions': u'rscwo',  # old
-                                      'ipapermtarget': u'rscwo',
-                                      'ipapermtargetfilter': u'rscwo',
-                                      'ipapermtargetto': u'rscwo',
diff --git a/SOURCES/0012-ipa-replica-install_--setup-adtrust_check_for_package_ipa-server-trust-ad_rhbz#1658294.patch b/SOURCES/0012-ipa-replica-install_--setup-adtrust_check_for_package_ipa-server-trust-ad_rhbz#1658294.patch
deleted file mode 100644
index c5bc02d..0000000
--- a/SOURCES/0012-ipa-replica-install_--setup-adtrust_check_for_package_ipa-server-trust-ad_rhbz#1658294.patch
+++ /dev/null
@@ -1,65 +0,0 @@
-From be968ea01adf1721b0afd7393872a8d311d89d0c Mon Sep 17 00:00:00 2001
-From: Florence Blanc-Renaud <flo@redhat.com>
-Date: Oct 24 2018 14:21:47 +0000
-Subject: ipa-replica-install --setup-adtrust: check for package ipa-server-trust-ad
-
-
-When adding the option --setup-adtrust to ipa-replica-install,
-we need to check that the package freeipa-server-trust-ad is
-installed.
-To avoid relying on OS-specific commands like yum, the check is instead
-ensuring that the file /usr/share/ipa/smb.conf.empty is present
-(this file is delivered by the package).
-When the check is unsuccessful, ipa-replica-install exits with an error
-message.
-
-Fixes: https://pagure.io/freeipa/issue/7602
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-
----
-
-diff --git a/ipaplatform/base/constants.py b/ipaplatform/base/constants.py
-index be832fe..c67b991 100644
---- a/ipaplatform/base/constants.py
-+++ b/ipaplatform/base/constants.py
-@@ -15,6 +15,7 @@ class BaseConstantsNamespace(object):
-     HTTPD_USER = "apache"
-     HTTPD_GROUP = "apache"
-     GSSPROXY_USER = "root"
-+    IPA_ADTRUST_PACKAGE_NAME = "freeipa-server-trust-ad"
-     IPA_DNS_PACKAGE_NAME = "freeipa-server-dns"
-     KDCPROXY_USER = "kdcproxy"
-     NAMED_USER = "named"
-diff --git a/ipaplatform/rhel/constants.py b/ipaplatform/rhel/constants.py
-index 945f3dc..72335ac 100644
---- a/ipaplatform/rhel/constants.py
-+++ b/ipaplatform/rhel/constants.py
-@@ -13,6 +13,7 @@ from ipaplatform.redhat.constants import RedHatConstantsNamespace
- 
- 
- class RHELConstantsNamespace(RedHatConstantsNamespace):
-+    IPA_ADTRUST_PACKAGE_NAME = "ipa-server-trust-ad"
-     IPA_DNS_PACKAGE_NAME = "ipa-server-dns"
- 
- constants = RHELConstantsNamespace()
-diff --git a/ipaserver/install/adtrustinstance.py b/ipaserver/install/adtrustinstance.py
-index 3a751cc..67317ee 100644
---- a/ipaserver/install/adtrustinstance.py
-+++ b/ipaserver/install/adtrustinstance.py
-@@ -72,6 +72,15 @@ def check_inst():
-                   "start the installation again")
-             return False
- 
-+    # Check that ipa-server-trust-ad package is installed,
-+    # by looking for the file /usr/share/ipa/smb.conf.empty
-+    if not os.path.exists(os.path.join(paths.USR_SHARE_IPA_DIR,
-+                                       "smb.conf.empty")):
-+        print("AD Trust requires the '%s' package" %
-+              constants.IPA_ADTRUST_PACKAGE_NAME)
-+        print("Please install the package and start the installation again")
-+        return False
-+
-     #TODO: Add check for needed samba4 libraries
- 
-     return True
-
diff --git a/SOURCES/0013-ipaldap_invalid_modlist_when_attribute_encoding_can_vary_rhbz#1658302.patch b/SOURCES/0013-ipaldap_invalid_modlist_when_attribute_encoding_can_vary_rhbz#1658302.patch
deleted file mode 100644
index 270cbdf..0000000
--- a/SOURCES/0013-ipaldap_invalid_modlist_when_attribute_encoding_can_vary_rhbz#1658302.patch
+++ /dev/null
@@ -1,72 +0,0 @@
-From 9e7e9c1014c10f838b341a45436aba0840ad5b07 Mon Sep 17 00:00:00 2001
-From: Fraser Tweedale <ftweedal@redhat.com>
-Date: Nov 07 2018 13:51:59 +0000
-Subject: ipaldap: avoid invalid modlist when attribute encoding differs
-
-
-ipaldap does not take into account the possibility of the attribute
-encoding returned by python-ldap differing from the attribute
-encoding produced by FreeIPA.  In particular this can occur with DNs
-with special characters that require escaping.  For example,
-python-ldap (or the underlying LDAP library) escapes special
-characters using hex encoding:
-
-  CN=Test Sub-CA 201604041620,OU=ftweedal,O=Red Hat\2C Inc.,L=Brisbane,C=AU
-
-Whereas FreeIPA, when encoding the DN, escapes the character
-directly:
-
-  CN=Test Sub-CA 201604041620,OU=ftweedal,O=Red Hat\, Inc.,L=Brisbane,C=AU
-
-Therefore it is possible to generate an invalid modlist. For
-example, during external CA certificate renewal, if the issuer DN
-includes a comma in one of the attribute values (as above), an
-invalid modlist will be generated:
-
-  [ (ldap.MOD_ADD, 'ipacaissuerdn',
-      [b'CN=Test Sub-CA 201604041620,OU=ftweedal,O=Red Hat\, Inc.,L=Brisbane,C=AU'])
-  , (ldap.MOD_DELETE, 'ipacaissuerdn',
-      [b'CN=Test Sub-CA 201604041620,OU=ftweedal,O=Red Hat\2C Inc.,L=Brisbane,C=AU'])
-  ]
-
-Although encoded differently, these are the same value.  If this
-modification is applied to the object, attributeOrValueExists (error
-20) occurs.
-
-To avoid the issue, put deletes before adds in the modlist.  If a
-value is present (with different encodings) as both an addition and
-a deletion, it must be because the original object contained the
-value with a different encoding.  Therefore it is safe to delete it,
-then add it back.
-
-Note that the modlist is not optimal.  In the simplest case (like
-above example), there should be no modification to perform.  It is
-considerably more complex (and more computation) to implement this
-because the raw attribute values must be decoded before comparison.
-
-Fixes: https://pagure.io/freeipa/issue/7750
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
-index fbc824e..cf1e60b 100644
---- a/ipapython/ipaldap.py
-+++ b/ipapython/ipaldap.py
-@@ -565,10 +565,13 @@ class LDAPEntry(MutableMapping):
-                     raise errors.OnlyOneValueAllowed(attr=name)
-                 modlist.append((ldap.MOD_REPLACE, name, adds))
-             else:
--                if adds:
--                    modlist.append((ldap.MOD_ADD, name, adds))
-+                # dels before adds, in case the same value occurs in
-+                # both due to encoding differences
-+                # (https://pagure.io/freeipa/issue/7750)
-                 if dels:
-                     modlist.append((ldap.MOD_DELETE, name, dels))
-+                if adds:
-+                    modlist.append((ldap.MOD_ADD, name, adds))
- 
-         # Usually the modlist order does not matter.
-         # However, for schema updates, we want 'attributetypes' before
-
diff --git a/SOURCES/0014-Allow_ipaapi_and_Apache_user_to_access_SSSD_IFP_rhbz#1639910.patch b/SOURCES/0014-Allow_ipaapi_and_Apache_user_to_access_SSSD_IFP_rhbz#1639910.patch
deleted file mode 100644
index 9a6f2b7..0000000
--- a/SOURCES/0014-Allow_ipaapi_and_Apache_user_to_access_SSSD_IFP_rhbz#1639910.patch
+++ /dev/null
@@ -1,816 +0,0 @@
-From 785c496dceb76a8f628249ce598e0540b1dfec6e Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Tue, 6 Nov 2018 13:57:14 +0100
-Subject: [PATCH] Allow ipaapi user to access SSSD's info pipe
-
-For smart card authentication, ipaapi must be able to access to sss-ifp.
-During installation and upgrade, the ipaapi user is now added to
-[ifp]allowed_uids.
-
-The commit also fixes two related issues:
-
-* The server upgrade code now enables ifp service in sssd.conf. The
-  existing code modified sssd.conf but never wrote the changes to disk.
-* sssd_enable_service() no longer fails after it has detected an
-  unrecognized service.
-
-Fixes: https://pagure.io/freeipa/issue/7751
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipaclient/install/client.py                | 41 ++++++++++++++++++----
- ipaserver/install/server/upgrade.py        | 27 +++++++++-----
- ipatests/test_integration/test_commands.py | 31 ++++++++++++++++
- 3 files changed, 83 insertions(+), 16 deletions(-)
-
-diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
-index 05255fe61b..f9b003ef57 100644
---- a/ipaclient/install/client.py
-+++ b/ipaclient/install/client.py
-@@ -35,6 +35,7 @@
- # pylint: enable=import-error
- 
- from ipalib import api, errors, x509
-+from ipalib.constants import IPAAPI_USER
- from ipalib.install import certmonger, certstore, service, sysrestore
- from ipalib.install import hostname as hostname_
- from ipalib.install.kinit import kinit_keytab, kinit_password
-@@ -914,7 +915,7 @@ def configure_sssd_conf(
-         domain = sssdconfig.new_domain(cli_domain)
- 
-     if options.on_master:
--        sssd_enable_service(sssdconfig, 'ifp')
-+        sssd_enable_ifp(sssdconfig)
- 
-     if (
-         (options.conf_ssh and os.path.isfile(paths.SSH_CONFIG)) or
-@@ -1018,21 +1019,47 @@ def configure_sssd_conf(
-     return 0
- 
- 
--def sssd_enable_service(sssdconfig, service):
-+def sssd_enable_service(sssdconfig, name):
-     try:
--        sssdconfig.new_service(service)
-+        sssdconfig.new_service(name)
-     except SSSDConfig.ServiceAlreadyExists:
-         pass
-     except SSSDConfig.ServiceNotRecognizedError:
-         logger.error(
--            "Unable to activate the %s service in SSSD config.", service)
-+            "Unable to activate the '%s' service in SSSD config.", name)
-         logger.info(
-             "Please make sure you have SSSD built with %s support "
--            "installed.", service)
-+            "installed.", name)
-         logger.info(
--            "Configure %s support manually in /etc/sssd/sssd.conf.", service)
-+            "Configure %s support manually in /etc/sssd/sssd.conf.", name)
-+        return None
- 
--    sssdconfig.activate_service(service)
-+    sssdconfig.activate_service(name)
-+    return sssdconfig.get_service(name)
-+
-+
-+def sssd_enable_ifp(sssdconfig):
-+    """Enable and configure libsss_simpleifp plugin
-+    """
-+    service = sssd_enable_service(sssdconfig, 'ifp')
-+    if service is None:
-+        # unrecognized service
-+        return
-+
-+    try:
-+        uids = service.get_option('allowed_uids')
-+    except SSSDConfig.NoOptionError:
-+        uids = set()
-+    else:
-+        uids = {s.strip() for s in uids.split(',') if s.strip()}
-+    # SSSD supports numeric and string UIDs
-+    # ensure that root is allowed to access IFP, might be 0 or root
-+    if uids.isdisjoint({'0', 'root'}):
-+        uids.add('root')
-+    # allow IPA API to access IFP
-+    uids.add(IPAAPI_USER)
-+    service.set_option('allowed_uids', ', '.join(sorted(uids)))
-+    sssdconfig.save_service(service)
- 
- 
- def change_ssh_config(filename, changes, sections):
-diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
-index 96c95b7a07..698afd347e 100644
---- a/ipaserver/install/server/upgrade.py
-+++ b/ipaserver/install/server/upgrade.py
-@@ -23,7 +23,7 @@
- import ipalib.util
- import ipalib.errors
- from ipaclient.install import timeconf
--from ipaclient.install.client import sssd_enable_service
-+from ipaclient.install.client import sssd_enable_ifp
- from ipaplatform import services
- from ipaplatform.tasks import tasks
- from ipapython import ipautil, version
-@@ -1408,6 +1408,22 @@ def set_sssd_domain_option(option, value):
-     sssdconfig.write(paths.SSSD_CONF)
- 
- 
-+def sssd_update():
-+    sssdconfig = SSSDConfig.SSSDConfig()
-+    sssdconfig.import_config()
-+    # upgrade domain
-+    domain = sssdconfig.get_domain(str(api.env.domain))
-+    domain.set_option('ipa_server_mode', 'True')
-+    domain.set_option('ipa_server', api.env.host)
-+    sssdconfig.save_domain(domain)
-+    # enable and configure IFP plugin
-+    sssd_enable_ifp(sssdconfig)
-+    # write config and restart service
-+    sssdconfig.write(paths.SSSD_CONF)
-+    sssd = services.service('sssd', api)
-+    sssd.restart()
-+
-+
- def remove_ds_ra_cert(subject_base):
-     logger.info('[Removing RA cert from DS NSS database]')
- 
-@@ -2017,15 +2033,8 @@ def upgrade_configuration():
-         cainstance.ensure_ipa_authority_entry()
- 
-     migrate_to_authselect()
--    set_sssd_domain_option('ipa_server_mode', 'True')
--    set_sssd_domain_option('ipa_server', api.env.host)
- 
--    sssdconfig = SSSDConfig.SSSDConfig()
--    sssdconfig.import_config()
--    sssd_enable_service(sssdconfig, 'ifp')
--
--    sssd = services.service('sssd', api)
--    sssd.restart()
-+    sssd_update()
- 
-     krb = krbinstance.KrbInstance(fstore)
-     krb.fqdn = fqdn
-diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py
-index 640eacfa06..1aa1bb3313 100644
---- a/ipatests/test_integration/test_commands.py
-+++ b/ipatests/test_integration/test_commands.py
-@@ -20,6 +20,8 @@
- from cryptography.hazmat.backends import default_backend
- from cryptography import x509
- 
-+from ipalib.constants import IPAAPI_USER
-+
- from ipaplatform.paths import paths
- 
- from ipatests.test_integration.base import IntegrationTest
-@@ -28,6 +30,7 @@
- 
- logger = logging.getLogger(__name__)
- 
-+
- class TestIPACommand(IntegrationTest):
-     """
-     A lot of commands can be executed against a single IPA installation
-@@ -429,3 +432,31 @@ def test_certificate_out_write_to_file(self):
-             x509.load_pem_x509_certificate(data, backend=default_backend())
- 
-             self.master.run_command(['rm', '-f', filename])
-+
-+    def test_sssd_ifp_access_ipaapi(self):
-+        # check that ipaapi is allowed to access sssd-ifp for smartcard auth
-+        # https://pagure.io/freeipa/issue/7751
-+        username = 'admin'
-+        # get UID for user
-+        result = self.master.run_command(['ipa', 'user-show', username])
-+        mo = re.search(r'UID: (\d+)', result.stdout_text)
-+        assert mo is not None, result.stdout_text
-+        uid = mo.group(1)
-+
-+        cmd = [
-+            'dbus-send',
-+            '--print-reply', '--system',
-+            '--dest=org.freedesktop.sssd.infopipe',
-+            '/org/freedesktop/sssd/infopipe/Users',
-+            'org.freedesktop.sssd.infopipe.Users.FindByName',
-+            'string:{}'.format(username)
-+        ]
-+        # test IFP as root
-+        result = self.master.run_command(cmd)
-+        assert uid in result.stdout_text
-+
-+        # test IFP as ipaapi
-+        result = self.master.run_command(
-+            ['sudo', '-u', IPAAPI_USER, '--'] + cmd
-+        )
-+        assert uid in result.stdout_text
-From eb0136ea3438b6fb1145456478f401b9b7467cba Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Fri, 16 Nov 2018 14:11:16 +0100
-Subject: [PATCH] Remove dead code
-
-set_sssd_domain_option() is no longer used. Changes are handled by
-sssd_update().
-
-See: https://pagure.io/freeipa/issue/7751
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipaserver/install/server/upgrade.py | 9 ---------
- 1 file changed, 9 deletions(-)
-
-diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
-index f1e78beb27..71bdd3670c 100644
---- a/ipaserver/install/server/upgrade.py
-+++ b/ipaserver/install/server/upgrade.py
-@@ -1399,15 +1399,6 @@ def fix_schema_file_syntax():
-     sysupgrade.set_upgrade_state('ds', 'fix_schema_syntax', True)
- 
- 
--def set_sssd_domain_option(option, value):
--    sssdconfig = SSSDConfig.SSSDConfig()
--    sssdconfig.import_config()
--    domain = sssdconfig.get_domain(str(api.env.domain))
--    domain.set_option(option, value)
--    sssdconfig.save_domain(domain)
--    sssdconfig.write(paths.SSSD_CONF)
--
--
- def sssd_update():
-     sssdconfig = SSSDConfig.SSSDConfig()
-     sssdconfig.import_config()
-From 415295a6f68f4c797529e19a3f0cf956619d4bed Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Fri, 16 Nov 2018 14:51:23 +0100
-Subject: [PATCH] Allow HTTPd user to access SSSD IFP
-
-For smart card and certificate authentication, Apache's
-mod_lookup_identity module must be able to acess SSSD IFP. The module
-accesses IFP as Apache user, not as ipaapi user.
-
-Apache is not allowed to use IFP by default. The update code uses the
-service's ok-to-auth-as-delegate flag to detect smart card / cert auth.
-
-See: https://pagure.io/freeipa/issue/7751
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipaclient/install/client.py         | 10 +++++++++-
- ipaserver/install/server/upgrade.py | 11 ++++++++++-
- 2 files changed, 19 insertions(+), 2 deletions(-)
-
-diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
-index f9b003ef57..6125588802 100644
---- a/ipaclient/install/client.py
-+++ b/ipaclient/install/client.py
-@@ -47,6 +47,7 @@
-     verify_host_resolvable,
- )
- from ipaplatform import services
-+from ipaplatform.constants import constants
- from ipaplatform.paths import paths
- from ipaplatform.tasks import tasks
- from ipapython import certdb, kernel_keyring, ipaldap, ipautil
-@@ -1038,8 +1039,13 @@ def sssd_enable_service(sssdconfig, name):
-     return sssdconfig.get_service(name)
- 
- 
--def sssd_enable_ifp(sssdconfig):
-+def sssd_enable_ifp(sssdconfig, allow_httpd=False):
-     """Enable and configure libsss_simpleifp plugin
-+
-+    Allow the ``ipaapi`` user to access IFP. In case allow_httpd is true,
-+    the Apache HTTPd user is also allowed to access IFP. For smart card
-+    authentication, mod_lookup_identity must be allowed to access user
-+    information.
-     """
-     service = sssd_enable_service(sssdconfig, 'ifp')
-     if service is None:
-@@ -1058,6 +1064,8 @@ def sssd_enable_ifp(sssdconfig):
-         uids.add('root')
-     # allow IPA API to access IFP
-     uids.add(IPAAPI_USER)
-+    if allow_httpd:
-+        uids.add(constants.HTTPD_USER)
-     service.set_option('allowed_uids', ', '.join(sorted(uids)))
-     sssdconfig.save_service(service)
- 
-diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
-index 71bdd3670c..4de7fd974d 100644
---- a/ipaserver/install/server/upgrade.py
-+++ b/ipaserver/install/server/upgrade.py
-@@ -1407,8 +1407,17 @@ def sssd_update():
-     domain.set_option('ipa_server_mode', 'True')
-     domain.set_option('ipa_server', api.env.host)
-     sssdconfig.save_domain(domain)
-+    # check if service has ok_to_auth_as_delegate
-+    service = 'HTTP/{}'.format(api.env.host)
-+    result = api.Command.service_show(service, all=True)
-+    flag = result['result'].get('ipakrboktoauthasdelegate', False)
-+    if flag:
-+        logger.debug(
-+            "%s has ok_to_auth_as_delegate, allow Apache to access IFP",
-+            services
-+        )
-     # enable and configure IFP plugin
--    sssd_enable_ifp(sssdconfig)
-+    sssd_enable_ifp(sssdconfig, allow_httpd=flag)
-     # write config and restart service
-     sssdconfig.write(paths.SSSD_CONF)
-     sssd = services.service('sssd', api)
-From d7d17ece57ae1322c8368b7853f24d56b1d6a150 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Fri, 16 Nov 2018 14:54:32 +0100
-Subject: [PATCH] Smart card auth advise: Allow Apache user
-
-Modify the smard card auth advise script to use sssd_enable_ifp() in
-order to allow Apache to access SSSD IFP.
-
-See: https://pagure.io/freeipa/issue/7751
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipaserver/advise/plugins/smart_card_auth.py | 21 ++++++++++++++++++++-
- 1 file changed, 20 insertions(+), 1 deletion(-)
-
-diff --git a/ipaserver/advise/plugins/smart_card_auth.py b/ipaserver/advise/plugins/smart_card_auth.py
-index 97e23303b0..9a7a315ed5 100644
---- a/ipaserver/advise/plugins/smart_card_auth.py
-+++ b/ipaserver/advise/plugins/smart_card_auth.py
-@@ -105,6 +105,7 @@ class config_server_for_smart_card_auth(common_smart_card_auth_config):
-     ssl_conf = paths.HTTPD_SSL_CONF
-     ssl_ocsp_directive = OCSP_DIRECTIVE
-     kdc_service_name = services.knownservices.krb5kdc.systemd_name
-+    httpd_service_name = services.knownservices.httpd.systemd_name
- 
-     def get_info(self):
-         self.log.exit_on_nonroot_euid()
-@@ -117,6 +118,7 @@ def get_info(self):
-         self.record_httpd_ocsp_status()
-         self.check_and_enable_pkinit()
-         self.enable_ok_to_auth_as_delegate_on_http_principal()
-+        self.allow_httpd_ifp()
-         self.upload_smartcard_ca_certificates_to_systemwide_db()
-         self.install_smart_card_signing_ca_certs()
-         self.update_ipa_ca_certificate_store()
-@@ -183,7 +185,9 @@ def _format_command(self, fmt_line, directive, filename):
- 
-     def restart_httpd(self):
-         self.log.comment('finally restart apache')
--        self.log.command('systemctl restart httpd')
-+        self.log.command(
-+            'systemctl restart {}'.format(self.httpd_service_name)
-+        )
- 
-     def record_httpd_ocsp_status(self):
-         self.log.comment('store the OCSP upgrade state')
-@@ -214,6 +218,21 @@ def enable_ok_to_auth_as_delegate_on_http_principal(self):
-             ["Failed to set OK_AS_AUTH_AS_DELEGATE flag on HTTP principal"]
-         )
- 
-+    def allow_httpd_ifp(self):
-+        self.log.comment('Allow Apache to access SSSD IFP')
-+        self.log.exit_on_failed_command(
-+            '{} -c "import SSSDConfig; '
-+            'from ipaclient.install.client import sssd_enable_ifp; '
-+            'from ipaplatform.paths import paths; '
-+            'c = SSSDConfig.SSSDConfig(); '
-+            'c.import_config(); '
-+            'sssd_enable_ifp(c, allow_httpd=True); '
-+            'c.write(paths.SSSD_CONF)"'.format(sys.executable),
-+            ['Failed to modify SSSD config']
-+        )
-+        self.log.comment('Restart sssd')
-+        self.log.command('systemctl restart sssd')
-+
-     def restart_kdc(self):
-         self.log.exit_on_failed_command(
-             'systemctl restart {}'.format(self.kdc_service_name),
-From b56db8daa704782c44683412b85a454654eabc19 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Mon, 19 Nov 2018 14:19:16 +0100
-Subject: [PATCH] Log stderr in run_command
-
-pytest_multihost's run_command() does not log stderr when a command
-fails. Wrap the function call to log stderr so it's easier to debug
-failing tests.
-
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipatests/pytest_ipa/integration/host.py | 19 +++++++++++++++++++
- 1 file changed, 19 insertions(+)
-
-diff --git a/ipatests/pytest_ipa/integration/host.py b/ipatests/pytest_ipa/integration/host.py
-index 28f6e2cd32..6aed58ae96 100644
---- a/ipatests/pytest_ipa/integration/host.py
-+++ b/ipatests/pytest_ipa/integration/host.py
-@@ -18,6 +18,7 @@
- # along with this program.  If not, see <http://www.gnu.org/licenses/>.
- 
- """Host class for integration testing"""
-+import subprocess
- 
- import pytest_multihost.host
- 
-@@ -60,6 +61,24 @@ def to_env(self, **kwargs):
-         from ipatests.pytest_ipa.integration.env_config import host_to_env
-         return host_to_env(self, **kwargs)
- 
-+    def run_command(self, argv, set_env=True, stdin_text=None,
-+                    log_stdout=True, raiseonerr=True,
-+                    cwd=None, bg=False, encoding='utf-8'):
-+        # Wrap run_command to log stderr on raiseonerr=True
-+        result = super().run_command(
-+            argv, set_env=set_env, stdin_text=stdin_text,
-+            log_stdout=log_stdout, raiseonerr=False, cwd=cwd, bg=bg,
-+            encoding=encoding
-+        )
-+        if result.returncode and raiseonerr:
-+            result.log.error('stderr: %s', result.stderr_text)
-+            raise subprocess.CalledProcessError(
-+                result.returncode, argv,
-+                result.stdout_text, result.stderr_text
-+            )
-+        else:
-+            return result
-+
- 
- class WinHost(pytest_multihost.host.WinHost):
-     """
-From 97776d2c4eed5de73780476bb11a635a2e47ebc5 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Wed, 21 Nov 2018 10:00:20 +0100
-Subject: [PATCH] Test smart card advise scripts
-
-Create and execute the server and client smart card advise scripts.
-
-See: See: https://pagure.io/freeipa/issue/7751
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipatests/prci_definitions/gating.yaml    |  2 +-
- ipatests/test_integration/test_advise.py | 99 ++++++++++++++++++------
- 2 files changed, 75 insertions(+), 26 deletions(-)
-
-diff --git a/ipatests/prci_definitions/gating.yaml b/ipatests/prci_definitions/gating.yaml
-index 7a9b612dea..13f8851d02 100644
---- a/ipatests/prci_definitions/gating.yaml
-+++ b/ipatests/prci_definitions/gating.yaml
-@@ -157,7 +157,7 @@ jobs:
-         test_suite: test_integration/test_advise.py
-         template: *ci-master-f29
-         timeout: 3600
--        topology: *master_1repl
-+        topology: *master_1repl_1client
- 
-   fedora-29/test_testconfig:
-     requires: [fedora-29/build]
-diff --git a/ipatests/test_integration/test_advise.py b/ipatests/test_integration/test_advise.py
-index 3b821c8797..b548614922 100644
---- a/ipatests/test_integration/test_advise.py
-+++ b/ipatests/test_integration/test_advise.py
-@@ -21,11 +21,17 @@
- # pylint: disable=no-member
- 
- import re
-+
-+from ipalib.constants import IPAAPI_USER
-+from ipaplatform.paths import paths
-+from ipaplatform.constants import constants
-+
-+from ipatests.create_external_ca import ExternalCA
- from ipatests.pytest_ipa.integration import tasks
- from ipatests.test_integration.base import IntegrationTest
- 
- 
--def run_advice(master, advice_id, advice_regex, raiseerr):
-+def run_advice(master, advice_id, advice_regex, raiseerr=True):
-     # Obtain the advice from the server
-     tasks.kinit_admin(master)
-     result = master.run_command(['ipa-advise', advice_id],
-@@ -43,28 +49,38 @@ class TestAdvice(IntegrationTest):
-     """
-     Tests ipa-advise output.
-     """
--    advice_id = None
--    raiseerr = None
--    advice_regex = ''
-     topology = 'line'
-+    num_replicas = 0
-+    num_clients = 1
-+
-+    def execute_advise(self, host, advice_id, *args):
-+        # ipa-advise script is only available on a server
-+        tasks.kinit_admin(self.master)
-+        advice = self.master.run_command(['ipa-advise', advice_id])
-+        # execute script on host (client or master)
-+        if host is not self.master:
-+            tasks.kinit_admin(host)
-+        filename = tasks.upload_temp_contents(host, advice.stdout_text)
-+        cmd = ['sh', filename]
-+        cmd.extend(args)
-+        try:
-+            result = host.run_command(cmd)
-+        finally:
-+            host.run_command(['rm', '-f', filename])
-+        return advice, result
- 
-     def test_invalid_advice(self):
-         advice_id = r'invalid-advise-param'
-         advice_regex = r"invalid[\s]+\'advice\'.*"
--        raiseerr = False
--
--        run_advice(self.master, advice_id, advice_regex, raiseerr)
--
-+        run_advice(self.master, advice_id, advice_regex, raiseerr=False)
- 
-     def test_advice_FreeBSDNSSPAM(self):
-         advice_id = 'config-freebsd-nss-pam-ldapd'
-         advice_regex = r"\#\!\/bin\/sh.*" \
-                        r"pkg_add[\s]+\-r[\s]+nss\-pam\-ldapd[\s]+curl.*" \
-                        r"\/usr\/local\/etc\/rc\.d\/nslcd[\s]+restart"
--        raiseerr = True
--
--        run_advice(self.master, advice_id, advice_regex, raiseerr)
- 
-+        run_advice(self.master, advice_id, advice_regex)
- 
-     def test_advice_GenericNSSPAM(self):
-         advice_id = 'config-generic-linux-nss-pam-ldapd'
-@@ -75,20 +91,16 @@ def test_advice_GenericNSSPAM(self):
-             r"service[\s]+nscd[\s]+stop[\s]+\&\&[\s]+service[\s]+"
-             r"nslcd[\s]+restart"
-         )
--        raiseerr = True
--
--        run_advice(self.master, advice_id, advice_regex, raiseerr)
- 
-+        run_advice(self.master, advice_id, advice_regex)
- 
-     def test_advice_GenericSSSDBefore19(self):
-         advice_id = r'config-generic-linux-sssd-before-1-9'
-         advice_regex = r"\#\!\/bin\/sh.*" \
-                        r"apt\-get[\s]+\-y[\s]+install sssd curl openssl.*" \
-                        r"service[\s]+sssd[\s]+start"
--        raiseerr = True
--
--        run_advice(self.master, advice_id, advice_regex, raiseerr)
- 
-+        run_advice(self.master, advice_id, advice_regex)
- 
-     def test_advice_RedHatNSS(self):
-         advice_id = 'config-redhat-nss-ldap'
-@@ -100,10 +112,8 @@ def test_advice_RedHatNSS(self):
-             r"[\s]+\-\-enableldapauth[\s]+"
-             r"\-\-ldapserver=.*[\s]+\-\-ldapbasedn=.*"
-         )
--        raiseerr = True
--
--        run_advice(self.master, advice_id, advice_regex, raiseerr)
- 
-+        run_advice(self.master, advice_id, advice_regex)
- 
-     def test_advice_RedHatNSSPAM(self):
-         advice_id = 'config-redhat-nss-pam-ldapd'
-@@ -113,10 +123,8 @@ def test_advice_RedHatNSSPAM(self):
-                        r"authconfig[\s]+\-\-updateall[\s]+\-\-enableldap"\
-                        r"[\s]+\-\-enableldaptls[\s]+\-\-enableldapauth[\s]+" \
-                        r"\-\-ldapserver=.*[\s]+\-\-ldapbasedn=.*"
--        raiseerr = True
--
--        run_advice(self.master, advice_id, advice_regex, raiseerr)
- 
-+        run_advice(self.master, advice_id, advice_regex)
- 
-     def test_advice_RedHatSSSDBefore19(self):
-         advice_id = 'config-redhat-sssd-before-1-9'
-@@ -125,6 +133,47 @@ def test_advice_RedHatSSSDBefore19(self):
-             r"yum[\s]+install[\s]+\-y[\s]+sssd[\s]+authconfig[\s]+"
-             r"curl[\s]+openssl.*service[\s]+sssd[\s]+start"
-         )
--        raiseerr = True
- 
--        run_advice(self.master, advice_id, advice_regex, raiseerr)
-+        run_advice(self.master, advice_id, advice_regex)
-+
-+    # trivial checks
-+    def test_advice_enable_admins_sudo(self):
-+        advice_id = 'enable_admins_sudo'
-+        advice_regex = r"\#\!\/bin\/sh.*"
-+        run_advice(self.master, advice_id, advice_regex)
-+
-+    def test_advice_config_server_for_smart_card_auth(self):
-+        advice_id = 'config_server_for_smart_card_auth'
-+        advice_regex = r"\#\!\/bin\/sh.*"
-+        run_advice(self.master, advice_id, advice_regex)
-+
-+        ca_pem = ExternalCA().create_ca()
-+        ca_file = tasks.upload_temp_contents(self.master, ca_pem)
-+        try:
-+            self.execute_advise(self.master, advice_id, ca_file)
-+        except Exception:
-+            # debug: sometimes ipa-certupdate times out in
-+            # "Resubmitting certmonger request"
-+            self.master.run_command(['getcert', 'list'])
-+            raise
-+        finally:
-+            self.master.run_command(['rm', '-f', ca_file])
-+        sssd_conf = self.master.get_file_contents(
-+            paths.SSSD_CONF, encoding='utf-8'
-+        )
-+        assert constants.HTTPD_USER in sssd_conf
-+        assert IPAAPI_USER in sssd_conf
-+
-+    def test_advice_config_client_for_smart_card_auth(self):
-+        advice_id = 'config_client_for_smart_card_auth'
-+        advice_regex = r"\#\!\/bin\/sh.*"
-+        run_advice(self.master, advice_id, advice_regex)
-+
-+        client = self.clients[0]
-+
-+        ca_pem = ExternalCA().create_ca()
-+        ca_file = tasks.upload_temp_contents(client, ca_pem)
-+        try:
-+            self.execute_advise(client, advice_id, ca_file)
-+        finally:
-+            client.run_command(['rm', '-f', ca_file])
-From 6ed90a2ac08c070e8e5c47a1eb3c52d7d30cabb8 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Wed, 21 Nov 2018 10:44:55 +0100
-Subject: [PATCH] Add install/remove package helpers to advise
-
-The smart card advise scripts assume that yum is installed. However
-Fedora has dnf and the yum wrapper is not installed by default.
-Installation and removal of packages is now provided by two helper
-methods that detect the package manager.
-
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipaserver/advise/base.py                    | 36 +++++++++++++++++++++
- ipaserver/advise/plugins/smart_card_auth.py | 24 +++++++-------
- 2 files changed, 47 insertions(+), 13 deletions(-)
-
-diff --git a/ipaserver/advise/base.py b/ipaserver/advise/base.py
-index 07b1431e84..ec65113e34 100644
---- a/ipaserver/advise/base.py
-+++ b/ipaserver/advise/base.py
-@@ -227,6 +227,7 @@ def __init__(self):
-         self.content = []
-         self.prefix = '# '
-         self.options = None
-+        self.pkgmgr_detected = False
-         self._indentation_tracker = _IndentationTracker(
-             spaces_per_indent=DEFAULT_INDENTATION_INCREMENT)
- 
-@@ -312,6 +313,41 @@ def exit_on_predicate(self, predicate, error_message_lines):
- 
-             self.command('exit 1')
- 
-+    def detect_pkgmgr(self):
-+        self.commands_on_predicate(
-+            'which yum >/dev/null',
-+            commands_to_run_when_true=['PKGMGR=yum'],
-+            commands_to_run_when_false=['PKGMGR=dnf']
-+        )
-+        self.pkgmgr_detected = True
-+
-+    def install_packages(self, names, error_message_lines):
-+        assert isinstance(names, list)
-+        self.detect_pkgmgr()
-+        self.command('rpm -qi {} > /dev/null'.format(' '.join(names)))
-+        self.commands_on_predicate(
-+            '[ "$?" -ne "0" ]',
-+            ['$PKGMGR install -y {}'.format(' '.join(names))]
-+        )
-+        self.exit_on_predicate(
-+            '[ "$?" -ne "0" ]',
-+            error_message_lines
-+        )
-+
-+    def remove_package(self, name, error_message_lines):
-+        # remove only supports one package name
-+        assert ' ' not in name
-+        self.detect_pkgmgr()
-+        self.command('rpm -qi {} > /dev/null'.format(name))
-+        self.commands_on_predicate(
-+            '[ "$?" -eq "0" ]',
-+            ['$PKGMGR remove -y {} || exit 1'.format(name)]
-+        )
-+        self.exit_on_predicate(
-+            '[ "$?" -ne "0" ]',
-+            error_message_lines
-+        )
-+
-     @contextmanager
-     def unbranched_if(self, predicate):
-         with self._compound_statement(UnbranchedIfStatement, predicate):
-diff --git a/ipaserver/advise/plugins/smart_card_auth.py b/ipaserver/advise/plugins/smart_card_auth.py
-index 9a7a315ed5..411124f935 100644
---- a/ipaserver/advise/plugins/smart_card_auth.py
-+++ b/ipaserver/advise/plugins/smart_card_auth.py
-@@ -135,9 +135,10 @@ def resolve_ipaca_records(self):
- 
-         self.log.comment('make sure bind-utils are installed so that we can '
-                          'dig for ipa-ca records')
--        self.log.exit_on_failed_command(
--            'yum install -y bind-utils',
--            ['Failed to install bind-utils'])
-+        self.log.install_packages(
-+            ['bind-utils'],
-+            ['Failed to install bind-utils']
-+        )
- 
-         self.log.comment('make sure ipa-ca records are resolvable, '
-                          'otherwise error out and instruct')
-@@ -272,26 +273,23 @@ def get_info(self):
-         self.restart_sssd()
- 
-     def check_and_remove_pam_pkcs11(self):
--        self.log.command('rpm -qi pam_pkcs11 > /dev/null')
--        self.log.commands_on_predicate(
--            '[ "$?" -eq "0" ]',
--            [
--                'yum remove -y pam_pkcs11'
--            ]
-+        self.log.remove_package(
-+            'pam_pkcs11',
-+            ['Could not remove pam_pkcs11 package']
-         )
- 
-     def install_opensc_and_dconf_packages(self):
-         self.log.comment(
-             'authconfig often complains about missing dconf, '
-             'install it explicitly')
--        self.log.exit_on_failed_command(
--            'yum install -y {} dconf'.format(self.opensc_module_name.lower()),
-+        self.log.install_packages(
-+            [self.opensc_module_name.lower(), 'dconf'],
-             ['Could not install OpenSC package']
-         )
- 
-     def install_krb5_client_dependencies(self):
--        self.log.exit_on_failed_command(
--            'yum install -y krb5-pkinit-openssl',
-+        self.log.install_packages(
-+            ['krb5-pkinit-openssl'],
-             ['Failed to install Kerberos client PKINIT extensions.']
-         )
- 
-From e05ce4a20d2395179580db7e3db75c601c8f364c Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Thu, 13 Dec 2018 14:40:44 +0100
-Subject: [PATCH] Python 2 compatibility
-
-Make new test helpers and test code compatible with Python 2.7.
-
-See: https://pagure.io/freeipa/issue/7751
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- ipatests/pytest_ipa/integration/host.py  | 4 ++--
- ipatests/test_integration/test_advise.py | 2 ++
- 2 files changed, 4 insertions(+), 2 deletions(-)
-
-diff --git a/ipatests/pytest_ipa/integration/host.py b/ipatests/pytest_ipa/integration/host.py
-index 6aed58ae96..eb05872467 100644
---- a/ipatests/pytest_ipa/integration/host.py
-+++ b/ipatests/pytest_ipa/integration/host.py
-@@ -65,7 +65,7 @@ def run_command(self, argv, set_env=True, stdin_text=None,
-                     log_stdout=True, raiseonerr=True,
-                     cwd=None, bg=False, encoding='utf-8'):
-         # Wrap run_command to log stderr on raiseonerr=True
--        result = super().run_command(
-+        result = super(Host, self).run_command(
-             argv, set_env=set_env, stdin_text=stdin_text,
-             log_stdout=log_stdout, raiseonerr=False, cwd=cwd, bg=bg,
-             encoding=encoding
-@@ -74,7 +74,7 @@ def run_command(self, argv, set_env=True, stdin_text=None,
-             result.log.error('stderr: %s', result.stderr_text)
-             raise subprocess.CalledProcessError(
-                 result.returncode, argv,
--                result.stdout_text, result.stderr_text
-+                result.stderr_text
-             )
-         else:
-             return result
-diff --git a/ipatests/test_integration/test_advise.py b/ipatests/test_integration/test_advise.py
-index b548614922..761f278238 100644
---- a/ipatests/test_integration/test_advise.py
-+++ b/ipatests/test_integration/test_advise.py
-@@ -20,6 +20,8 @@
- # FIXME: Pylint errors
- # pylint: disable=no-member
- 
-+from __future__ import absolute_import
-+
- import re
- 
- from ipalib.constants import IPAAPI_USER
diff --git a/SOURCES/0015-Add_sysadm_r_to_default_SELinux_user_map_order_1853e2e_rhbz#1658303.patch b/SOURCES/0015-Add_sysadm_r_to_default_SELinux_user_map_order_1853e2e_rhbz#1658303.patch
deleted file mode 100644
index 47c9744..0000000
--- a/SOURCES/0015-Add_sysadm_r_to_default_SELinux_user_map_order_1853e2e_rhbz#1658303.patch
+++ /dev/null
@@ -1,61 +0,0 @@
-From 1853e2ecd6b5cbe389507a8c3fc751deaf512bb6 Mon Sep 17 00:00:00 2001
-From: François Cami <fcami@redhat.com>
-Date: Nov 12 2018 07:52:27 +0000
-Subject: Add sysadm_r to default SELinux user map order
-
-
-It is a standard SELinux user role included in RHEL (like
-user_r, staff_r, guest_r) and used quite often.
-
-Fixes: https://pagure.io/freeipa/issue/7658
-Signed-off-by: François Cami <fcami@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
-
----
-
-diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
-index ea1e5b2..8cc79d1 100644
---- a/install/share/bootstrap-template.ldif
-+++ b/install/share/bootstrap-template.ldif
-@@ -411,7 +411,7 @@ ipaDefaultEmailDomain: $DOMAIN
- ipaMigrationEnabled: FALSE
- ipaConfigString: AllowNThash
- ipaConfigString: KDC:Disable Last Success
--ipaSELinuxUserMapOrder: guest_u:s0$$xguest_u:s0$$user_u:s0$$staff_u:s0-s0:c0.c1023$$unconfined_u:s0-s0:c0.c1023
-+ipaSELinuxUserMapOrder: guest_u:s0$$xguest_u:s0$$user_u:s0$$staff_u:s0-s0:c0.c1023$$sysadm_u:s0-s0:c0.c1023$$unconfined_u:s0-s0:c0.c1023
- ipaSELinuxUserMapDefault: unconfined_u:s0-s0:c0.c1023
- 
- dn: cn=cosTemplates,cn=accounts,$SUFFIX
-diff --git a/install/ui/test/data/ipa_init.json b/install/ui/test/data/ipa_init.json
-index 71c9d73..4298f7d 100644
---- a/install/ui/test/data/ipa_init.json
-+++ b/install/ui/test/data/ipa_init.json
-@@ -36,7 +36,7 @@
-                   "ipausers"
-                ],
-                "ipaselinuxusermaporder" : [
--                  "guest_u:s0$xguest_u:s0$user_u:s0$staff_u:s0-s0:c0.c1023$unconfined_u:s0-s0:c0.c1023"
-+                  "guest_u:s0$xguest_u:s0$user_u:s0$staff_u:s0-s0:c0.c1023$sysadm_u:s0-s0:c0.c1023$unconfined_u:s0-s0:c0.c1023"
-                ],
-                "ca_renewal_master_server" : [
-                   "vm.example.com"
-diff --git a/ipatests/test_xmlrpc/test_config_plugin.py b/ipatests/test_xmlrpc/test_config_plugin.py
-index 049e44d..cb8cdeb 100644
---- a/ipatests/test_xmlrpc/test_config_plugin.py
-+++ b/ipatests/test_xmlrpc/test_config_plugin.py
-@@ -148,8 +148,12 @@ class test_config(Declarative):
- 
-         dict(
-             desc='Try to set new selinux order and invalid default user',
--            command=('config_mod', [],
--                dict(ipaselinuxusermaporder=u'xguest_u:s0$guest_u:s0$user_u:s0-s0:c0.c1023$staff_u:s0-s0:c0.c1023$unconfined_u:s0-s0:c0.c1023',
-+            command=(
-+                'config_mod', [],
-+                dict(
-+                    ipaselinuxusermaporder=u'xguest_u:s0$guest_u:s0'
-+                    u'$user_u:s0-s0:c0.c1023$staff_u:s0-s0:c0.c1023'
-+                    u'$sysadm_u:s0-s0:c0.c1023$unconfined_u:s0-s0:c0.c1023',
-                     ipaselinuxusermapdefault=u'unknown_u:s0')),
-             expected=errors.ValidationError(name='ipaselinuxusermapdefault',
-                 error='SELinux user map default user not in order list'),
-
diff --git a/SOURCES/0016-certdb_non-empty_Subject_Key_Identifier_and_validate_server_cert_sig_rhbz#1641988.patch b/SOURCES/0016-certdb_non-empty_Subject_Key_Identifier_and_validate_server_cert_sig_rhbz#1641988.patch
deleted file mode 100644
index 4ad95c1..0000000
--- a/SOURCES/0016-certdb_non-empty_Subject_Key_Identifier_and_validate_server_cert_sig_rhbz#1641988.patch
+++ /dev/null
@@ -1,78 +0,0 @@
-From c7cc9896e89b3214c439e5601bf93b405dc1c72b Mon Sep 17 00:00:00 2001
-From: Fraser Tweedale <ftweedal@redhat.com>
-Date: Mon, 12 Nov 2018 16:40:38 +1100
-Subject: [PATCH] certdb: ensure non-empty Subject Key Identifier
-
-Installation or IPA CA renewal with externally-signed CA accepts an
-IPA CA certificate with empty Subject Key Identifier. This is
-technically legal in X.509, but is an operational issue.
-Furthermore, due to an extant bug in Dogtag
-(https://pagure.io/dogtagpki/issue/3079) it will cause Dogtag
-startup failure.
-
-Reject CA certificates with empty Subject Key Identifier.
-
-Fixes: https://pagure.io/freeipa/issue/7762
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- ipapython/certdb.py | 5 ++++-
- 1 file changed, 4 insertions(+), 1 deletion(-)
-
-diff --git a/ipapython/certdb.py b/ipapython/certdb.py
-index e3f00c2561..bef6809b0f 100644
---- a/ipapython/certdb.py
-+++ b/ipapython/certdb.py
-@@ -919,10 +919,13 @@ def verify_ca_cert_validity(self, nickname):
-             raise ValueError("not a CA certificate")
- 
-         try:
--            cert.extensions.get_extension_for_class(
-+            ski = cert.extensions.get_extension_for_class(
-                     cryptography.x509.SubjectKeyIdentifier)
-         except cryptography.x509.ExtensionNotFound:
-             raise ValueError("missing subject key identifier extension")
-+        else:
-+            if len(ski.value.digest) == 0:
-+                raise ValueError("subject key identifier must not be empty")
- 
-         try:
-             self.run_certutil(['-V', '-n', nickname, '-u', 'L'],
-From c2ae6380b3f6b3804ebd2a7dd2b159b779eb756c Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Tue, 13 Nov 2018 12:21:21 +0100
-Subject: [PATCH] certdb: validate server cert signature
-
-PR https://github.com/freeipa/freeipa/pull/2554 added the '-e' option for CA
-cert validation. Let's also verify signature, key size, and signing algorithm
-of server certs. With the '-e' option, the installer and other
-tools will catch weak certs early.
-
-Fixes: pagure.io/freeipa/issue/7761
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Fraser Tweedale <ftweedal@redhat.com>
----
- ipapython/certdb.py | 11 +++++++++--
- 1 file changed, 9 insertions(+), 2 deletions(-)
-
-diff --git a/ipapython/certdb.py b/ipapython/certdb.py
-index 05ec932985..1a92a12c50 100644
---- a/ipapython/certdb.py
-+++ b/ipapython/certdb.py
-@@ -891,8 +891,15 @@ def verify_server_cert_validity(self, nickname, hostname):
-         cert = self.get_cert(nickname)
- 
-         try:
--            self.run_certutil(['-V', '-n', nickname, '-u', 'V'],
--                              capture_output=True)
-+            self.run_certutil(
-+                [
-+                    '-V',  # check validity of cert and attrs
-+                    '-n', nickname,
-+                    '-u', 'V',  # usage; 'V' means "SSL server"
-+                    '-e',  # check signature(s); this checks
-+                    # key sizes, sig algorithm, etc.
-+                ],
-+                capture_output=True)
-         except ipautil.CalledProcessError as e:
-             # certutil output in case of error is
-             # 'certutil: certificate is invalid: <ERROR_STRING>\n'
diff --git a/SOURCES/0017-ipa-replica-install_password_and_admin-password_options_mutually_exclusive_rhbz#1658309.patch b/SOURCES/0017-ipa-replica-install_password_and_admin-password_options_mutually_exclusive_rhbz#1658309.patch
deleted file mode 100644
index 8015afd..0000000
--- a/SOURCES/0017-ipa-replica-install_password_and_admin-password_options_mutually_exclusive_rhbz#1658309.patch
+++ /dev/null
@@ -1,84 +0,0 @@
-From fd3f5153beb3221be077f277b07d886b6ca53b10 Mon Sep 17 00:00:00 2001
-From: Florence Blanc-Renaud <flo@redhat.com>
-Date: Nov 21 2018 03:21:29 +0000
-Subject: ipa-replica-install: password and admin-password options mutually exclusive
-
-
-Currently it is possible to run ipa-replica-install in one step,
-and provide --password and --admin-password simultaneously.
-This is confusing as --password is intended for one-time pwd
-when the ipa-replica-install command is delegated to a user
-who doesn't know the admin password.
-
-The fix makes --password and --admin-password options
-mutually exclusive.
-
-Fixes https://pagure.io/freeipa/issue/6353
-
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
-index 03d096d..d3e28a1 100644
---- a/ipaserver/install/server/replicainstall.py
-+++ b/ipaserver/install/server/replicainstall.py
-@@ -771,6 +771,10 @@ def promote_check(installer):
- 
-     client_fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
-     if not client_fstore.has_files():
-+        # One-step replica installation
-+        if options.password and options.admin_password:
-+            raise ScriptError("--password and --admin-password options are "
-+                              "mutually exclusive")
-         ensure_enrolled(installer)
-     else:
-         if (options.domain_name or options.server or options.realm_name or
-
-From 2b155f98e7b9ced739233242ff53e2d4b4c7f063 Mon Sep 17 00:00:00 2001
-From: Florence Blanc-Renaud <flo@redhat.com>
-Date: Nov 21 2018 03:21:29 +0000
-Subject: ipatests: add test for ipa-replica-install options
-
-
-Add a test checking that --password and --admin-password
-options are mutually exclusive.
-
-Related to https://pagure.io/freeipa/issue/6353
-
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/ipatests/test_integration/test_replica_promotion.py b/ipatests/test_integration/test_replica_promotion.py
-index 7803c34..e665318 100644
---- a/ipatests/test_integration/test_replica_promotion.py
-+++ b/ipatests/test_integration/test_replica_promotion.py
-@@ -50,6 +50,26 @@ class TestReplicaPromotionLevel1(ReplicaPromotionBase):
-     domain_level = DOMAIN_LEVEL_1
- 
-     @replicas_cleanup
-+    def test_one_step_install_pwd_and_admin_pwd(self):
-+        """--password and --admin-password options are mutually exclusive
-+
-+        Test for ticket 6353
-+        """
-+        expected_err = "--password and --admin-password options are " \
-+                       "mutually exclusive"
-+        result = self.replicas[0].run_command([
-+            'ipa-replica-install', '-w',
-+            self.master.config.admin_password,
-+            '-p', 'OTPpwd',
-+            '-n', self.master.domain.name,
-+            '-r', self.master.domain.realm,
-+            '--server', self.master.hostname,
-+            '-U'],
-+            raiseonerr=False)
-+        assert result.returncode == 1
-+        assert expected_err in result.stderr_text
-+
-+    @replicas_cleanup
-     def test_one_command_installation(self):
-         """
-         TestCase:
-
diff --git a/SOURCES/0018-ipa_upgrade_handle_double-encoded_certificates_rhbz#1658310.patch b/SOURCES/0018-ipa_upgrade_handle_double-encoded_certificates_rhbz#1658310.patch
deleted file mode 100644
index 19ab348..0000000
--- a/SOURCES/0018-ipa_upgrade_handle_double-encoded_certificates_rhbz#1658310.patch
+++ /dev/null
@@ -1,153 +0,0 @@
-From 8ee3779ded64ff55c3981fb8c2db50cdcd3abc5b Mon Sep 17 00:00:00 2001
-From: Florence Blanc-Renaud <flo@redhat.com>
-Date: Nov 30 2018 14:20:59 +0000
-Subject: ipa upgrade: handle double-encoded certificates
-
-
-Issue is linked to the ticket
- #3477 LDAP upload CA cert sometimes double-encodes the value
-In old FreeIPA releases (< 3.2), the upgrade plugin was encoding twice
-the value of the certificate in cn=cacert,cn=ipa,cn=etc,$BASEDN.
-
-The fix for 3477 is only partial as it prevents double-encoding when a
-new cert is uploaded but does not fix wrong values already present in LDAP.
-
-With this commit, the code first tries to read a der cert. If it fails,
-it logs a debug message and re-writes the value caCertificate;binary
-to repair the entry.
-
-Fixes https://pagure.io/freeipa/issue/7775
-Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/ipaserver/install/plugins/upload_cacrt.py b/ipaserver/install/plugins/upload_cacrt.py
-index 85c67e7..763da1e 100644
---- a/ipaserver/install/plugins/upload_cacrt.py
-+++ b/ipaserver/install/plugins/upload_cacrt.py
-@@ -115,7 +115,18 @@ class update_upload_cacrt(Updater):
-                 entry.single_value['cACertificate;binary'] = ca_cert
-                 ldap.add_entry(entry)
-             else:
--                if b'' in entry['cACertificate;binary']:
-+                force_write = False
-+                try:
-+                    _cert_bin = entry['cACertificate;binary']
-+                except ValueError:
-+                    # BZ 1644874
-+                    # sometimes the cert is badly stored, twice encoded
-+                    # force write to fix the value
-+                    logger.debug('Fixing the value of cACertificate;binary '
-+                                 'in entry %s', entry.dn)
-+                    force_write = True
-+
-+                if force_write or b'' in entry['cACertificate;binary']:
-                     entry.single_value['cACertificate;binary'] = ca_cert
-                     ldap.update_entry(entry)
- 
-
-From 2b0f3a1abb9067a0a5ba8e59762bc41dc51608e2 Mon Sep 17 00:00:00 2001
-From: Florence Blanc-Renaud <flo@redhat.com>
-Date: Nov 30 2018 14:20:59 +0000
-Subject: ipatests: add upgrade test for double-encoded cacert
-
-
-Create a test for upgrade with the following scenario:
-- install master
-- write a double-encoded cert in the entry
-cn=cacert,,cn=ipa,cn=etc,$basedn
-to simulate bug 7775
-- call ipa-server-upgrade
-- check that the upgrade fixed the value
-
-The upgrade should finish successfully and repair
-the double-encoded cert.
-
-Related to https://pagure.io/freeipa/issue/7775
-
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/ipatests/test_integration/test_upgrade.py b/ipatests/test_integration/test_upgrade.py
-index cbf5f39..e0175bc 100644
---- a/ipatests/test_integration/test_upgrade.py
-+++ b/ipatests/test_integration/test_upgrade.py
-@@ -6,6 +6,9 @@
- Module provides tests to verify that the upgrade script works.
- """
- 
-+import base64
-+from cryptography.hazmat.primitives import serialization
-+from ipapython.dn import DN
- from ipatests.test_integration.base import IntegrationTest
- from ipatests.pytest_ipa.integration import tasks
- 
-@@ -21,3 +24,35 @@ class TestUpgrade(IntegrationTest):
-         assert ("DN: cn=Schema Compatibility,cn=plugins,cn=config does not \
-                 exists or haven't been updated" not in cmd.stdout_text)
-         assert cmd.returncode == 0
-+
-+    def test_double_encoded_cacert(self):
-+        """Test for BZ 1644874
-+
-+        In old IPA version, the entry cn=CAcert,cn=ipa,cn=etc,$basedn
-+        could contain a double-encoded cert, which leads to ipa-server-upgrade
-+        failure.
-+        Force a double-encoded value then call upgrade to check the fix.
-+        """
-+        # Read the current entry from LDAP
-+        ldap = self.master.ldap_connect()
-+        basedn = self.master.domain.basedn  # pylint: disable=no-member
-+        dn = DN(('cn', 'CAcert'), ('cn', 'ipa'), ('cn', 'etc'), basedn)
-+        entry = ldap.get_entry(dn)  # pylint: disable=no-member
-+        # Extract the certificate as DER then double-encode
-+        cacert = entry['cacertificate;binary'][0]
-+        cacert_der = cacert.public_bytes(serialization.Encoding.DER)
-+        cacert_b64 = base64.b64encode(cacert_der)
-+        # overwrite the value with double-encoded cert
-+        entry.single_value['cACertificate;binary'] = cacert_b64
-+        ldap.update_entry(entry)  # pylint: disable=no-member
-+
-+        # try the upgrade
-+        self.master.run_command(['ipa-server-upgrade'])
-+
-+        # read the value after upgrade, should be fixed
-+        entry = ldap.get_entry(dn)  # pylint: disable=no-member
-+        try:
-+            _cacert = entry['cacertificate;binary']
-+        except ValueError:
-+            raise AssertionError('%s contains a double-encoded cert'
-+                                 % entry.dn)
-
-From 2a299c786f93e67446d5fd227fe14884b4e0d293 Mon Sep 17 00:00:00 2001
-From: Florence Blanc-Renaud <flo@redhat.com>
-Date: Dec 06 2018 10:37:26 +0000
-Subject: ipatests: fix TestUpgrade::test_double_encoded_cacert
-
-
-The test is using a stale ldap connection to the master
-(obtained before calling upgrade, and the upgrade stops
-and starts 389-ds, breaking the connection).
-
-The fix re-connects before using the ldap handle.
-
-Related to https://pagure.io/freeipa/issue/7775
-
----
-
-diff --git a/ipatests/test_integration/test_upgrade.py b/ipatests/test_integration/test_upgrade.py
-index e0175bc..5cc890e 100644
---- a/ipatests/test_integration/test_upgrade.py
-+++ b/ipatests/test_integration/test_upgrade.py
-@@ -49,6 +49,8 @@ class TestUpgrade(IntegrationTest):
-         # try the upgrade
-         self.master.run_command(['ipa-server-upgrade'])
- 
-+        # reconnect to the master (upgrade stops 389-ds)
-+        ldap = self.master.ldap_connect()
-         # read the value after upgrade, should be fixed
-         entry = ldap.get_entry(dn)  # pylint: disable=no-member
-         try:
-
diff --git a/SOURCES/0019-PKINIT_fix_ipa-pkinit-manage_enable_disable_rhbz#1658313.patch b/SOURCES/0019-PKINIT_fix_ipa-pkinit-manage_enable_disable_rhbz#1658313.patch
deleted file mode 100644
index 58c0d41..0000000
--- a/SOURCES/0019-PKINIT_fix_ipa-pkinit-manage_enable_disable_rhbz#1658313.patch
+++ /dev/null
@@ -1,279 +0,0 @@
-From 940755e37b06ea95c32abd056277da19fb05ed3e Mon Sep 17 00:00:00 2001
-From: Florence Blanc-Renaud <flo@redhat.com>
-Date: Dec 06 2018 10:40:02 +0000
-Subject: ipatest: add test for ipa-pkinit-manage enable|disable
-
-
-Add a test for ipa-pkinit-manage with the following scenario:
-- install master with option --no-pkinit
-- call ipa-pkinit-manage enable
-- call ipa-pkinit-manage disable
-- call ipa-pkinit-manage enable
-
-At each step, check that the PKINIT cert is consistent with the
-expectations: when pkinit is enabled, the cert is signed by IPA
-CA and tracked by 'IPA' ca helper, but when pkinit is disabled,
-the cert is self-signed and tracked by 'SelfSign' CA helper.
-
-The new test is added in the nightly definitons.
-
-Related to https://pagure.io/freeipa/issue/7200
-
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-#diff --git a/ipatests/prci_definitions/nightly_f28.yaml b/ipatests/prci_definitions/nightly_f28.yaml
-#index ae8cacc..8462c14 100644
-#--- a/ipatests/prci_definitions/nightly_f28.yaml
-#+++ b/ipatests/prci_definitions/nightly_f28.yaml
-#@@ -639,3 +639,15 @@ jobs:
-#         template: *ci-master-f28
-#         timeout: 16000
-#         topology: *ipaserver
-#+
-#+  fedora-28/test_pkinit_manage:
-#+    requires: [fedora-28/build]
-#+    priority: 50
-#+    job:
-#+      class: RunPytest
-#+      args:
-#+        build_url: '{fedora-28/build_url}'
-#+        test_suite: test_integration/test_pkinit_manage.py
-#+        template: *ci-master-f28
-#+        timeout: 3600
-#+        topology: *master_1repl
-diff --git a/ipatests/prci_definitions/nightly_master.yaml b/ipatests/prci_definitions/nightly_master.yaml
-index 66921b6..3f2b346 100644
---- a/ipatests/prci_definitions/nightly_master.yaml
-+++ b/ipatests/prci_definitions/nightly_master.yaml
-@@ -639,3 +639,15 @@ jobs:
-         template: *ci-master-f29
-         timeout: 16000
-         topology: *ipaserver
-+
-+  fedora-29/test_pkinit_manage:
-+    requires: [fedora-29/build]
-+    priority: 50
-+    job:
-+      class: RunPytest
-+      args:
-+        build_url: '{fedora-29/build_url}'
-+        test_suite: test_integration/test_pkinit_manage.py
-+        template: *ci-master-f29
-+        timeout: 3600
-+        topology: *master_1repl
-diff --git a/ipatests/prci_definitions/nightly_rawhide.yaml b/ipatests/prci_definitions/nightly_rawhide.yaml
-index 24c26be..bdc34d2 100644
---- a/ipatests/prci_definitions/nightly_rawhide.yaml
-+++ b/ipatests/prci_definitions/nightly_rawhide.yaml
-@@ -627,3 +627,15 @@ jobs:
-         template: *ci-master-frawhide
-         timeout: 7200
-         topology: *ipaserver
-+
-+  fedora-rawhide/test_pkinit_manage:
-+    requires: [fedora-rawhide/build]
-+    priority: 50
-+    job:
-+      class: RunPytest
-+      args:
-+        build_url: '{fedora-rawhide/build_url}'
-+        test_suite: test_integration/test_pkinit_manage.py
-+        template: *ci-master-frawhide
-+        timeout: 3600
-+        topology: *master_1repl
-diff --git a/ipatests/test_integration/test_pkinit_manage.py b/ipatests/test_integration/test_pkinit_manage.py
-new file mode 100644
-index 0000000..bc1d9e3
---- /dev/null
-+++ b/ipatests/test_integration/test_pkinit_manage.py
-@@ -0,0 +1,111 @@
-+#
-+# Copyright (C) 2018  FreeIPA Contributors see COPYING for license
-+#
-+
-+"""
-+Module provides tests for the ipa-pkinit-manage command.
-+"""
-+
-+from __future__ import absolute_import
-+
-+from ipalib import x509
-+from ipaplatform.paths import paths
-+from ipapython.dn import DN
-+from ipatests.test_integration.base import IntegrationTest
-+from ipatests.pytest_ipa.integration import tasks
-+
-+
-+SELFSIGNED_CA_HELPER = 'SelfSign'
-+IPA_CA_HELPER = 'IPA'
-+PKINIT_STATUS_ENABLED = 'enabled'
-+PKINIT_STATUS_DISABLED = 'disabled'
-+
-+
-+def check_pkinit_status(host, status):
-+    """Ensures that ipa-pkinit-manage status returns the expected state"""
-+    result = host.run_command(['ipa-pkinit-manage', 'status'],
-+                              raiseonerr=False)
-+    assert result.returncode == 0
-+    assert 'PKINIT is {}'.format(status) in result.stdout_text
-+
-+
-+def check_pkinit_tracking(host, ca_helper):
-+    """Ensures that the PKINIT cert is tracked by the expected helper"""
-+    result = host.run_command(['getcert', 'list', '-f', paths.KDC_CERT],
-+                              raiseonerr=False)
-+    assert result.returncode == 0
-+    # Make sure that only one request exists
-+    assert result.stdout_text.count('Request ID') == 1
-+    # Make sure that the right CA helper is used to track the cert
-+    assert 'CA: {}'.format(ca_helper) in result.stdout_text
-+
-+
-+def check_pkinit_cert_issuer(host, issuer):
-+    """Ensures that the PKINIT cert is signed by the expected issuer"""
-+    data = host.get_file_contents(paths.KDC_CERT)
-+    pkinit_cert = x509.load_pem_x509_certificate(data)
-+    # Make sure that the issuer is the expected one
-+    assert DN(pkinit_cert.issuer) == DN(issuer)
-+
-+
-+def check_pkinit(host, enabled=True):
-+    """Checks that PKINIT is configured as expected
-+
-+    If enabled:
-+    ipa-pkinit-manage status must return 'PKINIT is enabled'
-+    the certificate must be tracked by IPA CA helper
-+    the certificate must be signed by IPA CA
-+    If disabled:
-+    ipa-pkinit-manage status must return 'PKINIT is disabled'
-+    the certificate must be tracked by SelfSign CA helper
-+    the certificate must be self-signed
-+    """
-+    if enabled:
-+        # When pkinit is enabled:
-+        # cert is tracked by IPA CA helper
-+        # cert is signed by IPA CA
-+        check_pkinit_status(host, PKINIT_STATUS_ENABLED)
-+        check_pkinit_tracking(host, IPA_CA_HELPER)
-+        check_pkinit_cert_issuer(
-+            host,
-+            'CN=Certificate Authority,O={}'.format(host.domain.realm))
-+    else:
-+        # When pkinit is disabled
-+        # cert is tracked by 'SelfSign' CA helper
-+        # cert is self-signed
-+        check_pkinit_status(host, PKINIT_STATUS_DISABLED)
-+        check_pkinit_tracking(host, SELFSIGNED_CA_HELPER)
-+        check_pkinit_cert_issuer(
-+            host,
-+            'CN={},O={}'.format(host.hostname, host.domain.realm))
-+
-+
-+class TestPkinitManage(IntegrationTest):
-+    """Tests the ipa-pkinit-manage command.
-+
-+    ipa-pkinit-manage can be used to enable, disable or check
-+    the status of PKINIT.
-+    When pkinit is enabled, the kerberos server is using a certificate
-+    signed either externally or by IPA CA. In the latter case, certmonger
-+    is tracking the cert with IPA helper.
-+    When pkinit is disabled, the kerberos server is using a self-signed
-+    certificate that is tracked by certmonger with the SelfSigned helper.
-+    """
-+
-+    @classmethod
-+    def install(cls, mh):
-+        # Install the master with PKINIT disabled
-+        tasks.install_master(cls.master, extra_args=['--no-pkinit'])
-+        check_pkinit(cls.master, enabled=False)
-+
-+    def test_pkinit_enable(self):
-+        self.master.run_command(['ipa-pkinit-manage', 'enable'])
-+        check_pkinit(self.master, enabled=True)
-+
-+    def test_pkinit_disable(self):
-+        self.master.run_command(['ipa-pkinit-manage', 'disable'])
-+        check_pkinit(self.master, enabled=False)
-+
-+    def test_pkinit_reenable(self):
-+        self.master.run_command(['ipa-pkinit-manage', 'enable'])
-+        check_pkinit(self.master, enabled=True)
-
-From ffa04a1862be198b9e1a5f6205d1ae0909ac5a4d Mon Sep 17 00:00:00 2001
-From: Florence Blanc-Renaud <flo@redhat.com>
-Date: Dec 06 2018 10:40:02 +0000
-Subject: PKINIT: fix ipa-pkinit-manage enable|disable
-
-
-The command ipa-pkinit-manage enable|disable is reporting
-success even though the PKINIT cert is not re-issued.
-The command triggers the request of a new certificate
-(signed by IPA CA when state=enable, selfsigned when disabled),
-but as the cert file is still present, certmonger does not create
-a new request and the existing certificate is kept.
-
-The fix consists in deleting the cert and key file before calling
-certmonger to request a new cert.
-
-There was also an issue in the is_pkinit_enabled() function:
-if no tracking request was found for the PKINIT cert,
-is_pkinit_enabled() was returning True while it should not.
-
-Fixes https://pagure.io/freeipa/issue/7200
-
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/ipaserver/install/ipa_pkinit_manage.py b/ipaserver/install/ipa_pkinit_manage.py
-index 4a79bba..86bd1ba 100644
---- a/ipaserver/install/ipa_pkinit_manage.py
-+++ b/ipaserver/install/ipa_pkinit_manage.py
-@@ -72,6 +72,8 @@ class PKINITManage(AdminTool):
-                 if ca_enabled:
-                     logger.warning(
-                         "Failed to stop tracking certificates: %s", e)
-+            # remove the cert and key
-+            krb.delete_pkinit_cert()
- 
-             krb.enable_ssl()
- 
-diff --git a/ipaserver/install/krbinstance.py b/ipaserver/install/krbinstance.py
-index 4ead1c5..850946a 100644
---- a/ipaserver/install/krbinstance.py
-+++ b/ipaserver/install/krbinstance.py
-@@ -77,7 +77,7 @@ def is_pkinit_enabled():
-     if os.path.exists(paths.KDC_CERT):
-         pkinit_request_ca = get_pkinit_request_ca()
- 
--        if pkinit_request_ca != "SelfSign":
-+        if pkinit_request_ca and pkinit_request_ca != "SelfSign":
-             return True
- 
-     return False
-@@ -602,6 +602,10 @@ class KrbInstance(service.Service):
-     def stop_tracking_certs(self):
-         certmonger.stop_tracking(certfile=paths.KDC_CERT)
- 
-+    def delete_pkinit_cert(self):
-+        installutils.remove_file(paths.KDC_CERT)
-+        installutils.remove_file(paths.KDC_KEY)
-+
-     def uninstall(self):
-         if self.is_configured():
-             self.print_msg("Unconfiguring %s" % self.service_name)
-@@ -627,8 +631,7 @@ class KrbInstance(service.Service):
-         # stop tracking and remove certificates
-         self.stop_tracking_certs()
-         installutils.remove_file(paths.CACERT_PEM)
--        installutils.remove_file(paths.KDC_CERT)
--        installutils.remove_file(paths.KDC_KEY)
-+        self.delete_pkinit_cert()
- 
-         if running:
-             self.restart()
-
diff --git a/SOURCES/0020-Enable_LDAP_debug_output_in_client_to_display_TLS_errors_in_join_rhbz#1658316.patch b/SOURCES/0020-Enable_LDAP_debug_output_in_client_to_display_TLS_errors_in_join_rhbz#1658316.patch
deleted file mode 100644
index 6b42e53..0000000
--- a/SOURCES/0020-Enable_LDAP_debug_output_in_client_to_display_TLS_errors_in_join_rhbz#1658316.patch
+++ /dev/null
@@ -1,134 +0,0 @@
-From be5513ba7d70cecba5aa7654b66c1aa4015f7de2 Mon Sep 17 00:00:00 2001
-From: Rob Crittenden <rcritten@redhat.com>
-Date: Tue, 9 Oct 2018 17:13:36 -0400
-Subject: [PATCH] Enable LDAP debug output in client to display TLS errors in
- join
-
-If ipa-join fails due to a TLS connection error when doing an
-LDAP-based enroll then nothing is logged by default except an
-Invalid Password error which is misleading (because the failure
-occurs during the bind).
-
-The only way that debugging would have been sufficient is if
-the user passed --debug to ipa-client-install which is not great.
-
-This log level is otherwise very quiet and only logs one or two
-lines on errors which is perfect.
-
-https://pagure.io/freeipa/issue/7728
-
-Signed-off-by: Rob Crittenden <rcritten@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- client/ipa-join.c | 64 ++++++++++++++++++++++++++---------------------
- 1 file changed, 35 insertions(+), 29 deletions(-)
-
-diff --git a/client/ipa-join.c b/client/ipa-join.c
-index 7f454f723d..750114896f 100644
---- a/client/ipa-join.c
-+++ b/client/ipa-join.c
-@@ -197,33 +197,31 @@ callRPC(char * user_agent,
- 
- /* The caller is responsible for unbinding the connection if ld is not NULL */
- static LDAP *
--connect_ldap(const char *hostname, const char *binddn, const char *bindpw) {
-+connect_ldap(const char *hostname, const char *binddn, const char *bindpw,
-+             int *ret) {
-     LDAP *ld = NULL;
--    int ret;
--    int ldapdebug = 0;
--    char *uri;
-+    int ldapdebug = 2;
-+    char *uri = NULL;
-     struct berval bindpw_bv;
- 
--    if (debug) {
--        ldapdebug = 2;
--        ret = ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &ldapdebug);
--        if (ret != LDAP_OPT_SUCCESS) {
--            goto fail;
--        }
-+    *ret = ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &ldapdebug);
-+    if (*ret != LDAP_OPT_SUCCESS) {
-+        goto fail;
-     }
- 
--    ret = asprintf(&uri, "ldaps://%s:636", hostname);
--    if (ret == -1) {
-+    *ret = asprintf(&uri, "ldaps://%s:636", hostname);
-+    if (*ret == -1) {
-         fprintf(stderr, _("Out of memory!"));
-+        *ret = LDAP_NO_MEMORY;
-         goto fail;
-     }
- 
--    ret = ipa_ldap_init(&ld, uri);
--    if (ret != LDAP_SUCCESS) {
-+    *ret = ipa_ldap_init(&ld, uri);
-+    if (*ret != LDAP_SUCCESS) {
-         goto fail;
-     }
--    ret = ipa_tls_ssl_init(ld, uri, DEFAULT_CA_CERT_FILE);
--    if (ret != LDAP_SUCCESS) {
-+    *ret = ipa_tls_ssl_init(ld, uri, DEFAULT_CA_CERT_FILE);
-+    if (*ret != LDAP_SUCCESS) {
-         fprintf(stderr, _("Unable to enable SSL in LDAP\n"));
-         goto fail;
-     }
-@@ -238,15 +236,11 @@ connect_ldap(const char *hostname, const char *binddn, const char *bindpw) {
-         bindpw_bv.bv_len = 0;
-     }
- 
--    ret = ldap_sasl_bind_s(ld, binddn, LDAP_SASL_SIMPLE, &bindpw_bv,
--                           NULL, NULL, NULL);
--
--    if (ret != LDAP_SUCCESS) {
--        int err;
-+    *ret = ldap_sasl_bind_s(ld, binddn, LDAP_SASL_SIMPLE, &bindpw_bv,
-+                            NULL, NULL, NULL);
- 
--        ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &err);
--        if (debug)
--            fprintf(stderr, _("Bind failed: %s\n"), ldap_err2string(err));
-+    if (*ret != LDAP_SUCCESS) {
-+        fprintf(stderr, _("Bind failed: %s\n"), ldap_err2string(*ret));
-         goto fail;
-     }
- 
-@@ -309,7 +303,7 @@ get_root_dn(const char *ipaserver, char **ldap_base)
-     struct berval **defvals;
-     int ret, rval = 0;
- 
--    ld = connect_ldap(ipaserver, NULL, NULL);
-+    ld = connect_ldap(ipaserver, NULL, NULL, &ret);
-     if (!ld) {
-         rval = 14;
-         goto done;
-@@ -429,11 +423,23 @@ join_ldap(const char *ipaserver, char *hostname, char ** binddn, const char *bin
-         rval = 3;
-         goto done;
-     }
--    ld = connect_ldap(ipaserver, *binddn, bindpw);
-+    ld = connect_ldap(ipaserver, *binddn, bindpw, &ret);
-     if (!ld) {
--        if (!quiet)
--            fprintf(stderr, _("Incorrect password.\n"));
--        rval = 15;
-+        if (quiet)
-+            goto done;
-+
-+        switch(ret) {
-+            case LDAP_NO_MEMORY:
-+                rval = 3;
-+                break;
-+            case LDAP_INVALID_CREDENTIALS: /* incorrect password */
-+            case LDAP_INAPPROPRIATE_AUTH: /* no password set */
-+                rval = 15;
-+                break;
-+            default: /* LDAP connection error catch-all */
-+                rval = 14;
-+                break;
-+        }
-         goto done;
-     }
- 
diff --git a/SOURCES/0021-rpc_always_read_response_rhbz#1639890.patch b/SOURCES/0021-rpc_always_read_response_rhbz#1639890.patch
deleted file mode 100644
index 627d233..0000000
--- a/SOURCES/0021-rpc_always_read_response_rhbz#1639890.patch
+++ /dev/null
@@ -1,58 +0,0 @@
-From 4c0e7d69e461a28a254e7c7a27c2768be3163a3d Mon Sep 17 00:00:00 2001
-From: Fraser Tweedale <ftweedal@redhat.com>
-Date: Wed, 7 Nov 2018 17:06:47 +1100
-Subject: [PATCH] rpc: always read response
-
-If the server responds 401 and the response body is empty, the
-client raises ResponseNotReady.  This occurs because:
-
-1. For a non-200 response, the response read only if the
-   Content-Length header occurs.
-
-2. The response must be read before another request (e.g. the
-   follow-up request with WWW-Authenticate header set), and this
-   condition was not met.  For details see
-   https://github.com/python/cpython/blob/v3.6.7/Lib/http/client.py#L1305-L1321.
-
-This situation should not arise in regular use, because the client
-either has a session cookie, or, knowing the details of the server
-it is contacting, it establishes the GSS-API context and includes
-the WWW-Authenticate header in the initial request.
-
-Nevertheless, this problem has been observed in the wild.  I do not
-know its ordinary cause(s), but one can force the issue by removing
-an authenticated user's session cache from /run/ipa/ccaches, then
-performing a request.
-
-Resolve the issue by always reading the response.  It is safe to
-call response.read() regardless of whether the Content-Length header
-appears, or whether the body is empty.
-
-Fixes: https://pagure.io/freeipa/issue/7752
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- ipalib/rpc.py | 11 +++++++++--
- 1 file changed, 9 insertions(+), 2 deletions(-)
-
-diff --git a/ipalib/rpc.py b/ipalib/rpc.py
-index b27f3cef9c..23841d0a4c 100644
---- a/ipalib/rpc.py
-+++ b/ipalib/rpc.py
-@@ -712,8 +712,15 @@ def single_request(self, host, handler, request_body, verbose=0):
-                     response = h.getresponse()
- 
-                 if response.status != 200:
--                    if (response.getheader("content-length", 0)):
--                        response.read()
-+                    # Must read response (even if it is empty)
-+                    # before sending another request.
-+                    #
-+                    # https://docs.python.org/3/library/http.client.html
-+                    #   #http.client.HTTPConnection.getresponse
-+                    #
-+                    # https://pagure.io/freeipa/issue/7752
-+                    #
-+                    response.read()
- 
-                     if response.status == 401:
-                         if not self._auth_complete(response):
diff --git a/SOURCES/0022-ipa_vault-retrieve_fix_internal_error_rhbz#1658485.patch b/SOURCES/0022-ipa_vault-retrieve_fix_internal_error_rhbz#1658485.patch
deleted file mode 100644
index 16ea8b9..0000000
--- a/SOURCES/0022-ipa_vault-retrieve_fix_internal_error_rhbz#1658485.patch
+++ /dev/null
@@ -1,136 +0,0 @@
-From 858859187a1353cbaa893642cc7b27f9f644b18b Mon Sep 17 00:00:00 2001
-From: François Cami <fcami@redhat.com>
-Date: Nov 23 2018 09:54:46 +0000
-Subject: Add a shared-vault-retrieve test
-
-
-Add a shared-vault-retrieve test when:
-* master has KRA installed
-* replica has no KRA
-This currently fails because of issue#7691
-
-Related-to: https://pagure.io/freeipa/issue/7691
-Signed-off-by: François Cami <fcami@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/ipatests/test_integration/test_vault.py b/ipatests/test_integration/test_vault.py
-index ea2591b..e5b3ad1 100644
---- a/ipatests/test_integration/test_vault.py
-+++ b/ipatests/test_integration/test_vault.py
-@@ -20,14 +20,17 @@ class TestInstallKRA(IntegrationTest):
- 
-     vault_password = "password"
-     vault_data = "SSBsb3ZlIENJIHRlc3RzCg=="
-+    vault_user = "vault_user"
-+    vault_user_password = "vault_user_password"
-     vault_name_master = "ci_test_vault_master"
-     vault_name_master2 = "ci_test_vault_master2"
-     vault_name_master3 = "ci_test_vault_master3"
-     vault_name_replica_without_KRA = "ci_test_vault_replica_without_kra"
-+    shared_vault_name_replica_without_KRA = ("ci_test_shared"
-+                                             "_vault_replica_without_kra")
-     vault_name_replica_with_KRA = "ci_test_vault_replica_with_kra"
-     vault_name_replica_KRA_uninstalled = "ci_test_vault_replica_KRA_uninstalled"
- 
--
-     @classmethod
-     def install(cls, mh):
-         tasks.install_master(cls.master, setup_kra=True)
-@@ -89,6 +92,66 @@ class TestInstallKRA(IntegrationTest):
- 
-         self._retrieve_secret([self.vault_name_replica_without_KRA])
- 
-+    def test_create_and_retrieve_shared_vault_replica_without_kra(self):
-+        # create vault
-+        self.replicas[0].run_command([
-+            "ipa", "vault-add",
-+            self.shared_vault_name_replica_without_KRA,
-+            "--shared",
-+            "--type", "standard",
-+        ])
-+
-+        # archive secret
-+        self.replicas[0].run_command([
-+            "ipa", "vault-archive",
-+            self.shared_vault_name_replica_without_KRA,
-+            "--shared",
-+            "--data", self.vault_data,
-+        ])
-+        time.sleep(WAIT_AFTER_ARCHIVE)
-+
-+        # add non-admin user
-+        self.replicas[0].run_command([
-+            'ipa', 'user-add', self.vault_user,
-+            '--first', self.vault_user,
-+            '--last', self.vault_user,
-+            '--password'],
-+            stdin_text=self.vault_user_password)
-+
-+        # add it to vault
-+        self.replicas[0].run_command([
-+            "ipa", "vault-add-member",
-+            self.shared_vault_name_replica_without_KRA,
-+            "--shared",
-+            "--users", self.vault_user,
-+        ])
-+
-+        self.replicas[0].run_command([
-+            'kdestroy', '-A'])
-+
-+        user_kinit = "%s\n%s\n%s\n" % (self.vault_user_password,
-+                                       self.vault_user_password,
-+                                       self.vault_user_password)
-+
-+        self.replicas[0].run_command([
-+            'kinit', self.vault_user],
-+            stdin_text=user_kinit)
-+
-+        # TODO: possibly refactor with:
-+        # self._retrieve_secret([self.vault_name_replica_without_KRA])
-+
-+        self.replicas[0].run_command([
-+            "ipa", "vault-retrieve",
-+            "--shared",
-+            self.shared_vault_name_replica_without_KRA,
-+            "--out=test.txt"])
-+
-+        self.replicas[0].run_command([
-+            'kdestroy', '-A'])
-+
-+        tasks.kinit_admin(self.replicas[0])
-+
-+
-     def test_create_and_retrieve_vault_replica_with_kra(self):
- 
-         # install KRA on replica
-
-From d57d97ea7f911e18ac75d532e19833c4efaafa96 Mon Sep 17 00:00:00 2001
-From: François Cami <fcami@redhat.com>
-Date: Nov 23 2018 09:54:46 +0000
-Subject: Add a "Find enabled services" ACI in 20-aci.update so that all users can find IPA servers and services. ACI suggested by Christian Heimes.
-
-
-Fixes: https://pagure.io/freeipa/issue/7691
-Signed-off-by: François Cami <fcami@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/install/updates/20-aci.update b/install/updates/20-aci.update
-index 184749d..7650cb4 100644
---- a/install/updates/20-aci.update
-+++ b/install/updates/20-aci.update
-@@ -36,6 +36,10 @@ remove:aci:(targetfilter="(objectclass=nsContainer)")(version 3.0; acl "Deny rea
- dn: cn=masters,cn=ipa,cn=etc,$SUFFIX
- add:aci:(targetfilter="(objectclass=nsContainer)")(targetattr="objectclass || cn")(version 3.0; acl "Read access to masters"; allow(read, search, compare) userdn = "ldap:///all";)
- 
-+# Allow users to discover enabled services
-+dn: cn=masters,cn=ipa,cn=etc,$SUFFIX
-+add:aci:(targetfilter = "(ipaConfigString=enabledService)")(targetattrs = "ipaConfigString")(version 3.0; acl "Find enabled services"; allow(read, search, compare) userdn = "ldap:///all";)
-+
- # Allow hosts to read masters service configuration
- dn: cn=masters,cn=ipa,cn=etc,$SUFFIX
- add:aci:(targetfilter = "(objectclass=nsContainer)")(targetattr = "ipaConfigString")(version 3.0; acl "Allow hosts to read masters service configuration"; allow(read, search, compare) userdn = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX";)
-
diff --git a/SOURCES/0023-Move_ipa_s_systemd_tmpfiles_from_var_run_to_run_rhbz#1658487.patch b/SOURCES/0023-Move_ipa_s_systemd_tmpfiles_from_var_run_to_run_rhbz#1658487.patch
deleted file mode 100644
index d4ec1e8..0000000
--- a/SOURCES/0023-Move_ipa_s_systemd_tmpfiles_from_var_run_to_run_rhbz#1658487.patch
+++ /dev/null
@@ -1,47 +0,0 @@
-From 13b6fec04582d43bfc057a4cc3dbb7e652c8a64f Mon Sep 17 00:00:00 2001
-From: Stanislav Levin <slev@altlinux.org>
-Date: Oct 15 2018 12:07:12 +0000
-Subject: Move ipa's systemd tmpfiles from /var/run to /run
-
-
-systemd 239 complains about the legacy of ipa's tmpfiles which
-are located on /var/run.
-
-Fixes: https://pagure.io/freeipa/issue/7732
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-
----
-
-diff --git a/init/tmpfilesd/Makefile.am b/init/tmpfilesd/Makefile.am
-index b2d91c3..3ea4533 100644
---- a/init/tmpfilesd/Makefile.am
-+++ b/init/tmpfilesd/Makefile.am
-@@ -7,4 +7,4 @@ systemdtmpfiles_DATA =         \
- CLEANFILES = $(systemdtmpfiles_DATA)
- 
- %: %.in Makefile
--	sed -e 's|@localstatedir[@]|$(localstatedir)|g' '$(srcdir)/$@.in' >$@
-+	cp '$(srcdir)/$@.in' $@
-diff --git a/init/tmpfilesd/ipa.conf.in b/init/tmpfilesd/ipa.conf.in
-index df66bef..183ceed 100644
---- a/init/tmpfilesd/ipa.conf.in
-+++ b/init/tmpfilesd/ipa.conf.in
-@@ -1,2 +1,2 @@
--d @localstatedir@/run/ipa 0711 root root
--d @localstatedir@/run/ipa/ccaches 0770 ipaapi ipaapi
-+d /run/ipa 0711 root root
-+d /run/ipa/ccaches 0770 ipaapi ipaapi
-diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
-index bc04964..8c1d44f 100644
---- a/ipaplatform/base/paths.py
-+++ b/ipaplatform/base/paths.py
-@@ -350,7 +350,7 @@ class BasePathNamespace(object):
-     OPENDNSSEC_KASP_DB = "/var/opendnssec/kasp.db"
-     IPA_ODS_EXPORTER_CCACHE = "/var/opendnssec/tmp/ipa-ods-exporter.ccache"
-     VAR_RUN_DIRSRV_DIR = "/var/run/dirsrv"
--    IPA_CCACHES = "/var/run/ipa/ccaches"
-+    IPA_CCACHES = "/run/ipa/ccaches"
-     HTTP_CCACHE = "/var/lib/ipa/gssproxy/http.ccache"
-     CA_BUNDLE_PEM = "/var/lib/ipa-client/pki/ca-bundle.pem"
-     KDC_CA_BUNDLE_PEM = "/var/lib/ipa-client/pki/kdc-ca-bundle.pem"
-
diff --git a/SOURCES/0024-Fix_authselect_invocations_to_work_with_1.0.2_rhbz#1654291.patch b/SOURCES/0024-Fix_authselect_invocations_to_work_with_1.0.2_rhbz#1654291.patch
deleted file mode 100644
index 63c6f05..0000000
--- a/SOURCES/0024-Fix_authselect_invocations_to_work_with_1.0.2_rhbz#1654291.patch
+++ /dev/null
@@ -1,78 +0,0 @@
-From 05c5be1b1c5ae63c5547d248d926b3411bff2733 Mon Sep 17 00:00:00 2001
-From: Adam Williamson <awilliam@redhat.com>
-Date: Nov 29 2018 15:58:38 +0000
-Subject: Fix authselect invocations to work with 1.0.2
-
-
-Since authselect 1.0.2, invoking an authselect command sequence
-like this:
-
-['authselect', 'sssd', '', '--force']
-
-does not work: authselect barfs on the empty string arg and
-errors out. We must only pass a features arg if we actually have
-some text to go in it.
-
-This broke uninstallation.
-
-In all cases, features are now passed as separate arguments instead of one
-argument separated by space.
-
-Fixes: https://pagure.io/freeipa/issue/7776
-Signed-off-by: Adam Williamson <awilliam@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-
----
-
-diff --git a/ipaplatform/redhat/authconfig.py b/ipaplatform/redhat/authconfig.py
-index 77ccc36..58cf7df 100644
---- a/ipaplatform/redhat/authconfig.py
-+++ b/ipaplatform/redhat/authconfig.py
-@@ -158,15 +158,26 @@ class RedHatAuthSelect(RedHatAuthToolBase):
-                 " ".join(args))
- 
-             profile = 'sssd'
--            features = ''
-+            features = []
-         else:
--            profile = \
--                statestore.restore_state('authselect', 'profile') or 'sssd'
--            features = \
--                statestore.restore_state('authselect', 'features_list') or ''
-+            profile = statestore.restore_state('authselect', 'profile')
-+            if not profile:
-+                profile = 'sssd'
-+            features_state = statestore.restore_state(
-+                'authselect', 'features_list'
-+            )
-             statestore.delete_state('authselect', 'mkhomedir')
-+            # only non-empty features, https://pagure.io/freeipa/issue/7776
-+            if features_state is not None:
-+                features = [
-+                    f.strip() for f in features_state.split(' ') if f.strip()
-+                ]
-+            else:
-+                features = []
- 
--        cmd = [paths.AUTHSELECT, "select", profile, features, "--force"]
-+        cmd = [paths.AUTHSELECT, "select", profile]
-+        cmd.extend(features)
-+        cmd.append("--force")
-         ipautil.run(cmd)
- 
-     def backup(self, path):
-@@ -186,10 +197,9 @@ class RedHatAuthSelect(RedHatAuthToolBase):
- 
-         if cfg:
-             profile = cfg[0]
--
--            cmd = [
--                paths.AUTHSELECT, "select", profile,
--                " ".join(cfg[1]), "--force"]
-+            cmd = [paths.AUTHSELECT, "select", profile]
-+            cmd.extend(cfg[1])
-+            cmd.append("--force")
-             ipautil.run(cmd)
- 
-     def set_nisdomain(self, nisdomain):
-
diff --git a/SOURCES/0025-ipa-client-automount_and_NFS_unit_name_changes_rhbz#1645501.patch b/SOURCES/0025-ipa-client-automount_and_NFS_unit_name_changes_rhbz#1645501.patch
deleted file mode 100644
index 935d25d..0000000
--- a/SOURCES/0025-ipa-client-automount_and_NFS_unit_name_changes_rhbz#1645501.patch
+++ /dev/null
@@ -1,291 +0,0 @@
-From 65f6c8dc2585144b17ff89e63e4ba300971996dd Mon Sep 17 00:00:00 2001
-From: =?UTF-8?q?Fran=C3=A7ois=20Cami?= <fcami@redhat.com>
-Date: Thu, 6 Dec 2018 16:10:00 +0100
-Subject: [PATCH] Fix NFS unit names
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-NFS unit names were renamed.
-Compatibility was maintained with older unit names
-through symlinks. When these symlinks are removed
-only new unit names work, so changing to using non-
-symlink unit names is required.
-
-Fixes: https://pagure.io/freeipa/issue/7783
-Signed-off-by: François Cami <fcami@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- ipaplatform/redhat/services.py | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/ipaplatform/redhat/services.py b/ipaplatform/redhat/services.py
-index 390bbb0231..20395aee44 100644
---- a/ipaplatform/redhat/services.py
-+++ b/ipaplatform/redhat/services.py
-@@ -45,8 +45,8 @@
- redhat_system_units = dict((x, "%s.service" % x)
-                            for x in base_services.wellknownservices)
- 
--redhat_system_units['rpcgssd'] = 'nfs-secure.service'
--redhat_system_units['rpcidmapd'] = 'nfs-idmap.service'
-+redhat_system_units['rpcgssd'] = 'rpc-gssd.service'
-+redhat_system_units['rpcidmapd'] = 'nfs-idmapd.service'
- redhat_system_units['domainname'] = 'nis-domainname.service'
- 
- # Rewrite dirsrv and pki-tomcatd services as they support instances via separate
-From 0687e4869995842a90d5d656749de42daceb2ad4 Mon Sep 17 00:00:00 2001
-From: =?UTF-8?q?Fran=C3=A7ois=20Cami?= <fcami@redhat.com>
-Date: Thu, 6 Dec 2018 17:29:26 +0100
-Subject: [PATCH] ipa-client-automount: use nfs-utils unit
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-- remove nfs-idmapd from units we enable & start as:
-  - it is not used on NFS clients anymore
-  - it is a static unit
-- remove rpc-gssd as well as it is a static unit
-- restart nfs-utils and rpc-gssd
-- manage systemctl-related exceptions during uninstall
-
-Fixes: https://pagure.io/freeipa/issue/7780
-Fixes: https://pagure.io/freeipa/issue/7781
-Signed-off-by: François Cami <fcami@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- client/ipa-client-automount.in | 55 +++++++++++-----------------------
- ipaplatform/base/services.py   |  3 +-
- 2 files changed, 20 insertions(+), 38 deletions(-)
- mode change 100644 => 100755 client/ipa-client-automount.in
-
-diff --git a/client/ipa-client-automount.in b/client/ipa-client-automount.in
-old mode 100644
-new mode 100755
-index 7348e20775..15926bd028
---- a/client/ipa-client-automount.in
-+++ b/client/ipa-client-automount.in
-@@ -314,23 +314,21 @@ def uninstall(fstore, statestore):
-                 print('Unable to restore SSSD configuration: %s' % str(e))
-                 logger.debug('Unable to restore SSSD configuration: %s',
-                              str(e))
-+ 
-+    # rpcidmapd and rpcgssd are static units now
-     if statestore.has_state('rpcidmapd'):
--        enabled = statestore.restore_state('rpcidmapd', 'enabled')
--        running = statestore.restore_state('rpcidmapd', 'running')
--        rpcidmapd = services.knownservices.rpcidmapd
--        if not enabled:
--            rpcidmapd.disable()
--        if not running:
--            rpcidmapd.stop()
-+        statestore.delete_state('rpcidmapd','enabled')
-+        statestore.delete_state('rpcidmapd','running')
-     if statestore.has_state('rpcgssd'):
--        enabled = statestore.restore_state('rpcgssd', 'enabled')
--        running = statestore.restore_state('rpcgssd', 'running')
--        rpcgssd = services.knownservices.rpcgssd
--        if not enabled:
--            rpcgssd.disable()
--        if not running:
--            rpcgssd.stop()
-+        statestore.delete_state('rpcgssd','enabled')
-+        statestore.delete_state('rpcgssd','running')
- 
-+    nfsutils = services.knownservices['nfs-utils']
-+    try:
-+        nfsutils.restart()
-+    except Exception as e:
-+        logger.error("Failed to restart nfs client services (%s)" % str(e))
-+        return 1
-     return 0
- 
- def configure_nfs(fstore, statestore):
-@@ -365,35 +363,18 @@ def configure_nfs(fstore, statestore):
- 
-     print("Configured %s" % paths.IDMAPD_CONF)
- 
--    rpcidmapd = services.knownservices.rpcidmapd
--    statestore.backup_state('rpcidmapd', 'enabled', rpcidmapd.is_enabled())
--    statestore.backup_state('rpcidmapd', 'running', rpcidmapd.is_running())
--    try:
--        rpcidmapd.restart()
--        print("Started %s" % rpcidmapd.service_name)
--    except Exception as e:
--        logger.error("%s failed to restart: %s", rpcidmapd.service_name, e)
--    try:
--        rpcidmapd.enable()
--    except Exception as e:
--        print("Failed to configure automatic startup of the %s daemon" % (rpcidmapd.service_name))
--        logger.error("Failed to enable automatic startup of the %s daemon: %s",
--                     rpcidmapd.service_name, str(e))
--
-     rpcgssd = services.knownservices.rpcgssd
--    statestore.backup_state('rpcgssd', 'enabled', rpcgssd.is_enabled())
--    statestore.backup_state('rpcgssd', 'running', rpcgssd.is_running())
-     try:
-         rpcgssd.restart()
--        print("Started %s" % rpcgssd.service_name)
-     except Exception as e:
--        logger.error("%s failed to restart: %s", rpcgssd.service_name, e)
-+        logger.error("Failed to restart rpc-gssd (%s)" % str(e))
-+        return 1
-+    nfsutils = services.knownservices['nfs-utils']
-     try:
--        rpcgssd.enable()
-+        nfsutils.restart()
-     except Exception as e:
--        print("Failed to configure automatic startup of the %s daemon" % (rpcgssd.service_name))
--        logger.error("Failed to enable automatic startup of the %s daemon: %s",
--                     rpcgssd.service_name, str(e))
-+        logger.error("Failed to restart nfs client services (%s)" % str(e))
-+        return 1
- 
- def main():
-     try:
-diff --git a/ipaplatform/base/services.py b/ipaplatform/base/services.py
-index 4533ad5b34..51c27848d7 100644
---- a/ipaplatform/base/services.py
-+++ b/ipaplatform/base/services.py
-@@ -53,7 +53,8 @@
-                      'dbus', 'nslcd', 'nscd', 'ntpd', 'portmap',
-                      'rpcbind', 'kadmin', 'sshd', 'autofs', 'rpcgssd',
-                      'rpcidmapd', 'pki_tomcatd', 'chronyd', 'domainname',
--                     'named', 'ods_enforcerd', 'ods_signerd', 'gssproxy']
-+                     'named', 'ods_enforcerd', 'ods_signerd', 'gssproxy',
-+                     'nfs-utils']
- 
- # The common ports for these services. This is used to wait for the
- # service to become available.
-From dfd741d3cd9c9d695e7ad6f88dcd4432fb73c126 Mon Sep 17 00:00:00 2001
-From: =?UTF-8?q?Fran=C3=A7ois=20Cami?= <fcami@redhat.com>
-Date: Mon, 10 Dec 2018 17:12:03 +0100
-Subject: [PATCH] ipatests: add a test for ipa-client-automount
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-Add an automount location then configure a client
-to use it. Only runs nightly.
-
-Related-to: https://pagure.io/freeipa/issue/7780
-Related-to: https://pagure.io/freeipa/issue/7781
-Related to: https://pagure.io/freeipa/issue/7783
-Signed-off-by: François Cami <fcami@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- ipatests/prci_definitions/nightly_master.yaml | 12 +++
- .../test_automount_locations.py               | 84 +++++++++++++++++++
- 2 files changed, 96 insertions(+)
- create mode 100644 ipatests/test_integration/test_automount_locations.py
-
-diff --git a/ipatests/prci_definitions/nightly_master.yaml b/ipatests/prci_definitions/nightly_master.yaml
-index 154e4c945d..b4dcc0870e 100644
---- a/ipatests/prci_definitions/nightly_master.yaml
-+++ b/ipatests/prci_definitions/nightly_master.yaml
-@@ -663,3 +663,15 @@ jobs:
-         template: *ci-master-f29
-         timeout: 3600
-         topology: *master_1repl
-+
-+  fedora-29/test_automount_locations:
-+    requires: [fedora-29/build]
-+    priority: 50
-+    job:
-+      class: RunPytest
-+      args:
-+        build_url: '{fedora-29/build_url}'
-+        test_suite: test_integration/test_automount_locations.py
-+        template: *ci-master-f29
-+        timeout: 6300
-+        topology: *master_1repl
-diff --git a/ipatests/test_integration/test_automount_locations.py b/ipatests/test_integration/test_automount_locations.py
-new file mode 100644
-index 0000000000..646d1d07a0
---- /dev/null
-+++ b/ipatests/test_integration/test_automount_locations.py
-@@ -0,0 +1,84 @@
-+#
-+# Copyright (C) 2018  FreeIPA Contributors see COPYING for license
-+#
-+
-+"""This module provides tests for the automount location feature.
-+"""
-+
-+from __future__ import absolute_import
-+
-+import time
-+import re
-+
-+from ipatests.test_integration.base import IntegrationTest
-+from ipatests.pytest_ipa.integration import tasks
-+
-+# give some time for units to stabilize
-+# otherwise we get transient errors
-+WAIT_AFTER_INSTALL = 5
-+WAIT_AFTER_UNINSTALL = WAIT_AFTER_INSTALL
-+
-+
-+class TestAutomountInstallUninstall(IntegrationTest):
-+    """
-+    Test if ipa-client-automount behaves as expected
-+    """
-+
-+    num_replicas = 1
-+    topology = 'star'
-+
-+    @classmethod
-+    def install(cls, mh):
-+        tasks.install_master(cls.master, setup_dns=False)
-+        client = cls.replicas[0]
-+        tasks.install_client(cls.master, client)
-+
-+    def test_use_automount_location(self):
-+
-+        client = self.replicas[0]
-+
-+        self.master.run_command([
-+            "ipa", "automountlocation-add", "baltimore"
-+        ])
-+
-+        self.master.run_command([
-+            "ipa", "host-mod", client.hostname,
-+            "--location", "baltimore"
-+        ])
-+
-+        # systemctl non-fatal errors will only be displayed
-+        # if ipa-client-automount is launched with --debug
-+        result1 = client.run_command([
-+            'ipa-client-automount', '--location', 'baltimore',
-+            '-U', '--debug'
-+        ])
-+
-+        # systemctl non-fatal errors will show up like this:
-+        # stderr=Failed to restart nfs-secure.service: \
-+        #        Unit nfs-secure.service not found.
-+        # normal output:
-+        # stderr=
-+        m1 = re.search(r'(?<=stderr\=Failed).+', result1.stderr_text)
-+        # maybe re-use m1.group(0) if it exists.
-+        assert m1 is None
-+
-+        time.sleep(WAIT_AFTER_INSTALL)
-+
-+        result2 = client.run_command([
-+            'ipa-client-automount', '--uninstall',
-+            '-U', '--debug'
-+        ])
-+
-+        m2 = re.search(r'(?<=stderr\=Failed).+', result2.stderr_text)
-+        assert m2 is None
-+
-+        time.sleep(WAIT_AFTER_UNINSTALL)
-+
-+        self.master.run_command([
-+            "ipa", "host-mod", client.hostname,
-+            "--location", "''"
-+        ])
-+
-+        self.master.run_command([
-+            "ipa", "automountlocation-del", "baltimore"
-+        ])
diff --git a/SOURCES/0026-Fix_compile_issue_with_new_389-ds_rhbz#1659448.patch b/SOURCES/0026-Fix_compile_issue_with_new_389-ds_rhbz#1659448.patch
deleted file mode 100644
index 640a3dd..0000000
--- a/SOURCES/0026-Fix_compile_issue_with_new_389-ds_rhbz#1659448.patch
+++ /dev/null
@@ -1,51 +0,0 @@
-From 2b30b637561eb56a1fb73164322c9a74c8365c0b Mon Sep 17 00:00:00 2001
-From: Alexander Bokovoy <abokovoy@redhat.com>
-Date: Fri, 14 Dec 2018 14:02:26 +0200
-Subject: [PATCH] ipa-sidgen: make internal fetch_attr helper really internal
-
-With 389-ds landing a change for
-https://pagure.io/389-ds-base/issue/49950, fetch_attr() helper function
-is exposed in slapi-plugin.h. However, in order to be able to build
-FreeIPA plugins against older 389-ds versions, prefer using a local
-variant of it.
-
-Rename fetch_attr() to ipa_sidgen_fetch_attr() so that it doesn't
-conflict at all.
-
-Fixes: https://pagure.io/freeipa/issue/7811
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- daemons/ipa-slapi-plugins/ipa-sidgen/ipa_sidgen_task.c | 6 +++---
- 1 file changed, 3 insertions(+), 3 deletions(-)
-
-diff --git a/daemons/ipa-slapi-plugins/ipa-sidgen/ipa_sidgen_task.c b/daemons/ipa-slapi-plugins/ipa-sidgen/ipa_sidgen_task.c
-index 9e474e83dd..007b1c945d 100644
---- a/daemons/ipa-slapi-plugins/ipa-sidgen/ipa_sidgen_task.c
-+++ b/daemons/ipa-slapi-plugins/ipa-sidgen/ipa_sidgen_task.c
-@@ -63,7 +63,7 @@ struct worker_ctx {
-     struct range_info **ranges;
- };
- 
--static const char *fetch_attr(Slapi_Entry *e, const char *attrname,
-+static const char *ipa_sidgen_fetch_attr(Slapi_Entry *e, const char *attrname,
-                                               const char *default_val)
- {
-     Slapi_Attr *attr;
-@@ -242,7 +242,7 @@ int sidgen_task_add(Slapi_PBlock *pb, Slapi_Entry *e,
- 
-     worker_ctx->plugin_id = global_sidgen_plugin_id;
- 
--    str = fetch_attr(e, "delay", NULL);
-+    str = ipa_sidgen_fetch_attr(e, "delay", NULL);
-     if (str != NULL) {
-         errno = 0;
-         worker_ctx->delay = strtol(str, &endptr, 10);
-@@ -255,7 +255,7 @@ int sidgen_task_add(Slapi_PBlock *pb, Slapi_Entry *e,
-     }
-     LOG("delay is [%li].\n", worker_ctx->delay);
- 
--    str = fetch_attr(e, "nsslapd-basedn", NULL);
-+    str = ipa_sidgen_fetch_attr(e, "nsslapd-basedn", NULL);
-     if (str == NULL) {
-         LOG_FATAL("Missing nsslapd-basedn!\n");
-         *returncode = LDAP_CONSTRAINT_VIOLATION;
diff --git a/SOURCES/0027-ipaserver-dcerpc-fix-exclusion-entry-with-a-forest-t.patch b/SOURCES/0027-ipaserver-dcerpc-fix-exclusion-entry-with-a-forest-t.patch
deleted file mode 100644
index 50cc957..0000000
--- a/SOURCES/0027-ipaserver-dcerpc-fix-exclusion-entry-with-a-forest-t.patch
+++ /dev/null
@@ -1,166 +0,0 @@
-From e5471e66c6a718ffa28433813b8a8d7896b16d9e Mon Sep 17 00:00:00 2001
-From: Alexander Bokovoy <abokovoy@redhat.com>
-Date: Mon, 7 Jan 2019 15:28:29 +0200
-Subject: [PATCH] ipaserver/dcerpc: fix exclusion entry with a forest trust
- domain info returned
-
-When looking through the topology of a trusted forest, we should support
-all types of forest trust records. Since Samba Python bindings parse the
-data into a typed structure, a type of the record has to be taken into
-account or there will be type mismatch when accessing elements of the
-union:
-
-        typedef [switch_type(lsa_ForestTrustRecordType)] union {
-                [case(LSA_FOREST_TRUST_TOP_LEVEL_NAME)] lsa_StringLarge top_level_name;
-                [case(LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX)] lsa_StringLarge top_level_name_ex;
-                [case(LSA_FOREST_TRUST_DOMAIN_INFO)] lsa_ForestTrustDomainInfo domain_info;
-                [default] lsa_ForestTrustBinaryData data;
-        } lsa_ForestTrustData;
-
-        typedef struct {
-                lsa_ForestTrustRecordFlags flags;
-                lsa_ForestTrustRecordType type;
-                NTTIME_hyper time;
-                [switch_is(type)] lsa_ForestTrustData forest_trust_data;
-        } lsa_ForestTrustRecord;
-
-        typedef [public] struct {
-                [range(0,4000)] uint32 count;
-                [size_is(count)] lsa_ForestTrustRecord **entries;
-        } lsa_ForestTrustInformation;
-
-Each entry in the lsa_ForestTrustInformation has forest_trust_data
-member but its content depends on the value of a type member
-(forest_trust_data is a union of all possible structures).
-
-Previously we assumed only TLN or TLN exclusion record which were
-of the same type (lsa_StringLarge). Access to forest_trust_data.string
-fails when forest_trust_data's type is lsa_ForestTrustDomainInfo as it
-has no string member.
-
-Fix the code by properly accessing the dns_domain_name from the
-lsa_ForestTrustDomainInfo structure.
-
-Fixes: https://pagure.io/freeipa/issue/7828
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
----
- ipaserver/dcerpc.py | 64 ++++++++++++++++++++++++++++++++++++++-------
- 1 file changed, 55 insertions(+), 9 deletions(-)
-
-diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
-index 125493657..51a8e82e7 100644
---- a/ipaserver/dcerpc.py
-+++ b/ipaserver/dcerpc.py
-@@ -51,6 +51,7 @@ from samba.dcerpc import security, lsa, drsblobs, nbt, netlogon
- from samba.ndr import ndr_pack, ndr_print
- from samba import net
- from samba import arcfour_encrypt
-+from samba import ntstatus
- import samba
- 
- import ldap as _ldap
-@@ -1105,6 +1106,25 @@ class TrustDomainInstance(object):
-         original forest.
-         """
- 
-+        def domain_name_from_ftinfo(ftinfo):
-+            """
-+            Returns a domain name string from a ForestTrustRecord
-+
-+            :param ftinfo: LSA ForestTrustRecord to parse
-+            """
-+            if ftinfo.type == lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
-+                return ftinfo.forest_trust_data.dns_domain_name.string
-+            elif ftinfo.type == lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME:
-+                return ftinfo.forest_trust_data.string
-+            elif ftinfo.type == lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
-+                # We should ignore TLN exclusion record because it
-+                # is already an exclusion so we aren't going to
-+                # change anything here
-+                return None
-+            else:
-+                # Ignore binary blobs we don't know about
-+                return None
-+
-         # List of entries for unsolved conflicts
-         result = []
- 
-@@ -1145,18 +1165,26 @@ class TrustDomainInstance(object):
-                     e1.time = e.time
-                     e1.forest_trust_data = e.forest_trust_data
- 
-+                    # We either have a domain struct, a TLN name,
-+                    # or a TLN exclusion name in the list.
-+                    # The rest we should skip, those are binary blobs
-+                    dns_domain_name = domain_name_from_ftinfo(e)
-+
-                     # Search for a match in the topology of another domain
-                     # if there is a match, we have to convert a record
-                     # into a TLN exclusion to allow its routing to the
-                     # another domain
-                     for r in another_domain.ftinfo_records:
--                        if r['rec_name'] == e.forest_trust_data.string:
-+                        # r['rec_name'] cannot be None, thus we can ignore
-+                        # the case when dns_domain_name is None
-+                        if r['rec_name'] == dns_domain_name:
-                             is_our_record = True
- 
-                             # Convert e1 into an exclusion record
-                             e1.type = lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX
-                             e1.flags = 0
-                             e1.time = trust_timestamp
-+                            e1.forest_trust_data.string = dns_domain_name
-                             break
-                     entries.append(e1)
- 
-@@ -1180,11 +1208,29 @@ class TrustDomainInstance(object):
-                 # Update the forest trust information now
-                 ldname = lsa.StringLarge()
-                 ldname.string = rec.name.string
--                cninfo = self._pipe.lsaRSetForestTrustInformation(
--                             self._policy_handle,
--                             ldname,
--                             lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
--                             fti, 0)
-+                cninfo = None
-+                try:
-+                    cninfo = self._pipe.lsaRSetForestTrustInformation(
-+                        self._policy_handle,
-+                        ldname,
-+                        lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
-+                        fti, 0)
-+                except samba.NTSTATUSError as error:
-+                    # Handle NT_STATUS_INVALID_PARAMETER separately
-+                    if ntstatus.NT_STATUS_INVALID_PARAMETER == error.args[0]:
-+                        result.append(rec)
-+                        logger.error("Unable to resolve conflict for "
-+                                     "DNS domain %s in the forest %s "
-+                                     "for in-forest domain %s. Trust cannot "
-+                                     "be established unless this conflict "
-+                                     "is fixed manually.",
-+                                     another_domain.info['dns_domain'],
-+                                     self.info['dns_domain'],
-+                                     rec.name.string)
-+                    else:
-+                        raise assess_dcerpc_error(error)
-+
-+
-                 if cninfo:
-                     result.append(rec)
-                     logger.error("When defining exception for DNS "
-@@ -1213,9 +1259,9 @@ class TrustDomainInstance(object):
-         # Otherwise, raise TrustTopologyConflictError() exception
-         domains = [x.name.string for x in result]
-         raise errors.TrustTopologyConflictError(
--                              target=self.info['dns_domain'],
--                              conflict=another_domain.info['dns_domain'],
--                              domains=domains)
-+            forest=self.info['dns_domain'],
-+            conflict=another_domain.info['dns_domain'],
-+            domains=domains)
- 
- 
- 
--- 
-2.20.1
-
diff --git a/SOURCES/0028-Create-systemd-user-HBAC-service-and-rule.patch b/SOURCES/0028-Create-systemd-user-HBAC-service-and-rule.patch
deleted file mode 100644
index 188d163..0000000
--- a/SOURCES/0028-Create-systemd-user-HBAC-service-and-rule.patch
+++ /dev/null
@@ -1,191 +0,0 @@
-From aaf938307acbe987f5e1effc2392894c22235013 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Fri, 11 Jan 2019 11:18:05 +0100
-Subject: [PATCH] Create systemd-user HBAC service and rule
-
-authselect changed pam_systemd session from optional to required. When
-the HBAC rule allow_all is disabled and replaced with more fine grained
-rules, loginsi now to fail, because systemd's user@.service is able to
-create a systemd session.
-
-Add systemd-user HBAC service and a HBAC rule that allows systemd-user
-to run on all hosts for all users by default. ipa-server-upgrade creates
-the service and rule, too. In case the service already exists, no
-attempt is made to create the rule. This allows admins to delete the
-rule permanently.
-
-See: https://bugzilla.redhat.com/show_bug.cgi?id=1643928
-Fixes: https://pagure.io/freeipa/issue/7831
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
----
- install/share/bootstrap-template.ldif      |  8 +++
- install/share/default-hbac.ldif            | 13 +++++
- ipaserver/install/server/upgrade.py        | 36 +++++++++++++
- ipatests/test_integration/test_commands.py | 59 ++++++++++++++++++++++
- 4 files changed, 116 insertions(+)
-
-diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
-index d48c4fafc..6cd17e37e 100644
---- a/install/share/bootstrap-template.ldif
-+++ b/install/share/bootstrap-template.ldif
-@@ -346,6 +346,14 @@ cn: sudo-i
- description: sudo-i
- ipauniqueid:autogenerate
- 
-+dn: cn=systemd-user,cn=hbacservices,cn=hbac,$SUFFIX
-+changetype: add
-+objectclass: ipahbacservice
-+objectclass: ipaobject
-+cn: systemd-user
-+description: pam_systemd and systemd user@.service
-+ipauniqueid:autogenerate
-+
- dn: cn=gdm,cn=hbacservices,cn=hbac,$SUFFIX
- changetype: add
- objectclass: ipahbacservice
-diff --git a/install/share/default-hbac.ldif b/install/share/default-hbac.ldif
-index 52fd30ec9..8dd90685c 100644
---- a/install/share/default-hbac.ldif
-+++ b/install/share/default-hbac.ldif
-@@ -12,3 +12,16 @@ ipaenabledflag: TRUE
- description: Allow all users to access any host from any host
- ipauniqueid: autogenerate
- 
-+# default HBAC policy for pam_systemd
-+dn: ipauniqueid=autogenerate,cn=hbac,$SUFFIX
-+changetype: add
-+objectclass: ipaassociation
-+objectclass: ipahbacrule
-+cn: allow_systemd-user
-+accessruletype: allow
-+usercategory: all
-+hostcategory: all
-+servicecategory: systemd-user
-+ipaenabledflag: TRUE
-+description: Allow pam_systemd to run user@.service to create a system user session
-+ipauniqueid: autogenerate
-diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
-index ae6fcc77e..3869bae3c 100644
---- a/ipaserver/install/server/upgrade.py
-+++ b/ipaserver/install/server/upgrade.py
-@@ -1735,6 +1735,41 @@ def migrate_to_authselect():
-     sysupgrade.set_upgrade_state('authcfg', 'migrated_to_authselect', True)
- 
- 
-+def add_systemd_user_hbac():
-+    logger.info('[Create systemd-user hbac service and rule]')
-+    rule = 'allow_systemd-user'
-+    service = 'systemd-user'
-+    try:
-+        api.Command.hbacsvc_add(
-+            service,
-+            description='pam_systemd and systemd user@.service'
-+        )
-+    except ipalib.errors.DuplicateEntry:
-+        logger.info('hbac service %s already exists', service)
-+        # Don't create hbac rule when hbacsvc already exists, so the rule
-+        # does not get re-created after it has been deleted by an admin.
-+        return
-+    else:
-+        logger.info('Created hbacsvc %s', service)
-+
-+    try:
-+        api.Command.hbacrule_add(
-+            rule,
-+            description=('Allow pam_systemd to run user@.service to create '
-+                         'a system user session'),
-+            usercategory='all',
-+            hostcategory='all',
-+        )
-+    except ipalib.errors.DuplicateEntry:
-+        logger.info('hbac rule %s already exists', rule)
-+    else:
-+        api.Command.hbacrule_add_service(
-+            rule,
-+            hbacsvc=(service,)
-+        )
-+        logger.info('Created hbac rule %s with hbacsvc=%s', rule, service)
-+
-+
- def fix_permissions():
-     """Fix permission of public accessible files and directories
- 
-@@ -2050,6 +2085,7 @@ def upgrade_configuration():
-         cainstance.ensure_ipa_authority_entry()
- 
-     migrate_to_authselect()
-+    add_systemd_user_hbac()
- 
-     sssd_update()
- 
-diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py
-index cfb2fa48d..1fb6450a2 100644
---- a/ipatests/test_integration/test_commands.py
-+++ b/ipatests/test_integration/test_commands.py
-@@ -462,3 +462,62 @@ class TestIPACommand(IntegrationTest):
-             ['sudo', '-u', IPAAPI_USER, '--'] + cmd
-         )
-         assert uid in result.stdout_text
-+
-+    def test_hbac_systemd_user(self):
-+        # https://pagure.io/freeipa/issue/7831
-+        tasks.kinit_admin(self.master)
-+        # check for presence
-+        self.master.run_command(
-+            ['ipa', 'hbacrule-show', 'allow_systemd-user']
-+        )
-+        self.master.run_command(
-+            ['ipa', 'hbacsvc-show', 'systemd-user']
-+        )
-+
-+        # delete both
-+        self.master.run_command(
-+            ['ipa', 'hbacrule-del', 'allow_systemd-user']
-+        )
-+        self.master.run_command(
-+            ['ipa', 'hbacsvc-del', 'systemd-user']
-+        )
-+
-+        # run upgrade
-+        result = self.master.run_command(['ipa-server-upgrade'])
-+        assert 'Created hbacsvc systemd-user' in result.stderr_text
-+        assert 'Created hbac rule allow_systemd-user' in result.stderr_text
-+
-+        # check for presence
-+        result = self.master.run_command(
-+            ['ipa', 'hbacrule-show', 'allow_systemd-user', '--all']
-+        )
-+        lines = set(l.strip() for l in result.stdout_text.split('\n'))
-+        assert 'User category: all' in lines
-+        assert 'Host category: all' in lines
-+        assert 'Enabled: TRUE' in lines
-+        assert 'Services: systemd-user' in lines
-+        assert 'accessruletype: allow' in lines
-+
-+        self.master.run_command(
-+            ['ipa', 'hbacsvc-show', 'systemd-user']
-+        )
-+
-+        # only delete rule
-+        self.master.run_command(
-+            ['ipa', 'hbacrule-del', 'allow_systemd-user']
-+        )
-+
-+        # run upgrade
-+        result = self.master.run_command(['ipa-server-upgrade'])
-+        assert (
-+            'hbac service systemd-user already exists' in result.stderr_text
-+        )
-+        assert (
-+            'Created hbac rule allow_systemd-user' not in result.stderr_text
-+        )
-+        result = self.master.run_command(
-+            ['ipa', 'hbacrule-show', 'allow_systemd-user'],
-+            raiseonerr=False
-+        )
-+        assert result.returncode != 0
-+        assert 'HBAC rule not found' in result.stderr_text
--- 
-2.20.1
-
diff --git a/SOURCES/0029-Resolve_user_group_names_in_idoverride_-find_rhbz#1657745.patch b/SOURCES/0029-Resolve_user_group_names_in_idoverride_-find_rhbz#1657745.patch
deleted file mode 100644
index 9d2cda5..0000000
--- a/SOURCES/0029-Resolve_user_group_names_in_idoverride_-find_rhbz#1657745.patch
+++ /dev/null
@@ -1,405 +0,0 @@
-BEGIN EXCERPT from 8182ebc6c3ca636276fc277186cfbff4ea9cf5c6 to have user_add
-in ipatests/pytest_ipa/integration/tasks.py to be able to apply the patch set.
-
-commit 8182ebc6c3ca636276fc277186cfbff4ea9cf5c6
-Author: Sergey Orlov <sorlov@redhat.com>
-Date:   Wed Nov 7 11:23:05 2018 +0100
-
-    ipatests: add test for ipa-restore in multi-master configuration
-    
-    Test ensures that after ipa-restore on the master, the replica can be
-    re-synchronized and a new replica can be created.
-    
-    https://pagure.io/freeipa/issue/7455
-    
-    Reviewed-By: Christian Heimes <cheimes@redhat.com>
-    Reviewed-By: Tibor Dudlak <tdudlak@redhat.com>
-
-diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
-index 814141b83..90da8fa62 100644
---- a/ipatests/pytest_ipa/integration/tasks.py
-+++ b/ipatests/pytest_ipa/integration/tasks.py
-@@ -1555,3 +1561,11 @@ def strip_cert_header(pem):
-         return s.group(1)
-     else:
-         return pem
-+
-+
-+def user_add(host, login):
-+    host.run_command([
-+        "ipa", "user-add", login,
-+        "--first", "test",
-+        "--last", "user"
-+    ])
-END EXCERPT
-From 5e6cb0ca034c711fe81fcfe7c651c5af3c65aa40 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Dec 07 2018 15:06:13 +0000
-Subject: Resolve user/group names in idoverride*-find
-
-
-ipa idoverrideuser-find and ...group-find have an --anchor argument. The
-anchor argument used to support only anchor UUIDs like
-':IPA:domain:UUID' or ':SID:S-sid'. The find commands now detect regular
-user or group names and translate them to anchors.
-
-Fixes: https://pagure.io/freeipa/issue/6594
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
-
----
-
-diff --git a/ipaserver/plugins/idviews.py b/ipaserver/plugins/idviews.py
-index 3252982..5213486 100644
---- a/ipaserver/plugins/idviews.py
-+++ b/ipaserver/plugins/idviews.py
-@@ -766,6 +766,40 @@ class baseidoverride(LDAPObject):
-                     error=_('Default Trust View cannot contain IPA users')
-                     )
- 
-+    def filter_for_anchor(self, ldap, filter, options, obj_type):
-+        """Modify filter to support user and group names
-+
-+        Allow users to pass in an IPA user/group name and resolve it to an
-+        anchor name.
-+
-+        :param ldap: ldap connection
-+        :param filter: pre_callback filter
-+        :param options: option dict
-+        :param obj_type: 'user' or 'group'
-+        :return: modified or same filter
-+        """
-+        anchor = options.get('ipaanchoruuid', None)
-+        # return original filter if anchor is absent or correct
-+        if anchor is None or ANCHOR_REGEX.match(anchor):
-+            return filter
-+        try:
-+            resolved_anchor = resolve_object_to_anchor(
-+                ldap, obj_type, anchor,
-+                options.get('fallback_to_ldap', False)
-+            )
-+        except (errors.NotFound, errors.ValidationError):
-+            # anchor cannot be resolved, let it pass through
-+            return filter
-+        else:
-+            return ldap.make_filter(
-+                {
-+                    'objectClass': self.object_class,
-+                    'ipaanchoruuid': resolved_anchor,
-+                },
-+                rules=ldap.MATCH_ALL
-+            )
-+
-+
- class baseidoverride_add(LDAPCreate):
-     __doc__ = _('Add a new ID override.')
-     msg_summary = _('Added ID override "%(value)s"')
-@@ -1128,6 +1162,15 @@ class idoverrideuser_find(baseidoverride_find):
-     msg_summary = ngettext('%(count)d User ID override matched',
-                            '%(count)d User ID overrides matched', 0)
- 
-+    def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args,
-+                     **options):
-+        result = super(idoverrideuser_find, self).pre_callback(
-+            ldap, filter, attrs_list, base_dn, scope, *args, **options
-+        )
-+        filter, base_dn, scope = result
-+        filter = self.obj.filter_for_anchor(ldap, filter, options, 'user')
-+        return filter, base_dn, scope
-+
-     def post_callback(self, ldap, entries, truncated, *args, **options):
-         truncated = super(idoverrideuser_find, self).post_callback(
-             ldap, entries, truncated, *args, **options)
-@@ -1173,6 +1216,15 @@ class idoverridegroup_find(baseidoverride_find):
-     msg_summary = ngettext('%(count)d Group ID override matched',
-                            '%(count)d Group ID overrides matched', 0)
- 
-+    def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args,
-+                     **options):
-+        result = super(idoverridegroup_find, self).pre_callback(
-+            ldap, filter, attrs_list, base_dn, scope, *args, **options
-+        )
-+        filter, base_dn, scope = result
-+        filter = self.obj.filter_for_anchor(ldap, filter, options, 'group')
-+        return filter, base_dn, scope
-+
- 
- @register()
- class idoverridegroup_show(baseidoverride_show):
-
-From 11b06d24a94c5e92a0275df759bc81f0fc81d802 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Dec 07 2018 15:06:13 +0000
-Subject: Add integration tests for idviews
-
-
-Add several tests to verify new anchor override and general idview
-override functionality.
-
-Fixes: https://pagure.io/freeipa/issue/6594
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
-
----
-
-diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
-index 36178e8..3548f2b 100644
---- a/ipatests/pytest_ipa/integration/tasks.py
-+++ b/ipatests/pytest_ipa/integration/tasks.py
-@@ -1576,9 +1576,19 @@ def strip_cert_header(pem):
-         return pem
- 
- 
--def user_add(host, login):
--    host.run_command([
-+def user_add(host, login, first='test', last='user', extra_args=()):
-+    cmd = [
-         "ipa", "user-add", login,
--        "--first", "test",
--        "--last", "user"
--    ])
-+        "--first", first,
-+        "--last", last
-+    ]
-+    cmd.extend(extra_args)
-+    return host.run_command(cmd)
-+
-+
-+def group_add(host, groupname, extra_args=()):
-+    cmd = [
-+        "ipa", "group-add", groupname,
-+    ]
-+    cmd.extend(extra_args)
-+    return host.run_command(cmd)
-diff --git a/ipatests/test_integration/test_idviews.py b/ipatests/test_integration/test_idviews.py
-index 9a8f379..6ede4d0 100644
---- a/ipatests/test_integration/test_idviews.py
-+++ b/ipatests/test_integration/test_idviews.py
-@@ -165,6 +165,7 @@ class TestRulesWithServicePrincipals(IntegrationTest):
- 
-     topology = 'star'
-     num_replicas = 0
-+    num_clients = 0
-     service_certprofile = 'caIPAserviceCert'
-     caacl = 'test_caacl'
-     keytab = "replica.keytab"
-@@ -238,3 +239,133 @@ EOF
-                                          raiseonerr=False)
-         assert(result.returncode == 0), (
-             'Failed to add a cert to custom certprofile')
-+
-+
-+class TestIDViews(IntegrationTest):
-+    topology = 'star'
-+    num_replicas = 0
-+    num_clients = 1
-+
-+    user1 = 'testuser1'
-+    user1_uid = 10001
-+    user1_gid = 10001
-+    user1_uid_override = 5001
-+    user1_gid_override = 6001
-+
-+    user2 = 'testuser2'
-+    user2_uid = 10002
-+    user2_gid = 10002
-+
-+    group1 = 'testgroup1'
-+    group1_gid = 11001
-+    group1_gid_override = 7001
-+
-+    idview = 'testview'
-+
-+    @classmethod
-+    def install(cls, mh):
-+        super(TestIDViews, cls).install(mh)
-+        master = cls.master
-+        client = cls.clients[0]
-+        tasks.kinit_admin(master)
-+
-+        tasks.user_add(
-+            master, cls.user1, first='Test1',
-+            extra_args=[
-+                '--uid', str(cls.user1_uid),
-+                '--gidnumber', str(cls.user1_gid),
-+            ]
-+        )
-+        tasks.user_add(
-+            master, cls.user2, first='Test2',
-+            extra_args=[
-+                '--uid', str(cls.user2_uid),
-+                '--gidnumber', str(cls.user2_gid),
-+            ]
-+        )
-+        tasks.group_add(
-+            master, cls.group1, extra_args=['--gid', str(cls.group1_gid)]
-+        )
-+
-+        master.run_command(['ipa', 'idview-add', cls.idview])
-+
-+        # add overrides for user1 and its default user group
-+        master.run_command([
-+            'ipa', 'idoverrideuser-add', cls.idview, cls.user1,
-+            '--uid', str(cls.user1_uid_override),
-+            '--gid', str(cls.user1_gid_override),
-+            '--homedir', '/special-home/{}'.format(cls.user1),
-+            '--shell', '/bin/special'
-+        ])
-+        master.run_command([
-+            'ipa', 'idoverridegroup-add', cls.idview, cls.group1,
-+            '--gid', str(cls.group1_gid_override),
-+        ])
-+
-+        # ID view overrides don't work on IPA masters
-+        master.run_command([
-+            'ipa', 'idview-apply', cls.idview,
-+            '--hosts', client.hostname
-+        ])
-+        # finally restart SSSD to materialize idviews
-+        client.run_command(['systemctl', 'restart', 'sssd.service'])
-+
-+    def test_useroverride(self):
-+        result = self.clients[0].run_command(['id', self.user1])
-+        assert 'uid={}'.format(self.user1_uid_override) in result.stdout_text
-+        assert 'gid={}'.format(self.user1_gid_override) in result.stdout_text
-+
-+        result = self.clients[0].run_command(
-+            ['getent', 'passwd', str(self.user1_uid_override)]
-+        )
-+        expected = '{}:*:{}:{}'.format(
-+            self.user1, self.user1_uid_override, self.user1_gid_override
-+        )
-+        assert expected in result.stdout_text
-+
-+        result = self.master.run_command(['id', self.user1])
-+        assert 'uid={}'.format(self.user1_uid) in result.stdout_text
-+        assert 'gid={}'.format(self.user1_gid) in result.stdout_text
-+
-+    def test_useroverride_original_uid(self):
-+        # It's still possible to request the user with its original UID. In
-+        # this case the getent command returns the user with override uid.
-+        result = self.clients[0].run_command(
-+            ['getent', 'passwd', str(self.user1_uid)]
-+        )
-+        expected = '{}:*:{}:{}'.format(
-+            self.user1, self.user1_uid_override, self.user1_gid_override
-+        )
-+        assert expected in result.stdout_text
-+
-+    def test_anchor_username(self):
-+        result = self.master.run_command([
-+            'ipa', 'idoverrideuser-find', self.idview, '--anchor', self.user1
-+        ])
-+        expected = "Anchor to override: {}".format(self.user1)
-+        assert expected in result.stdout_text
-+
-+    def test_groupoverride(self):
-+        result = self.clients[0].run_command(['getent', 'group', self.group1])
-+        assert ':{}:'.format(self.group1_gid_override) in result.stdout_text
-+
-+        result = self.master.run_command(['getent', 'group', self.group1])
-+        assert ':{}:'.format(self.group1_gid) in result.stdout_text
-+
-+    def test_groupoverride_system_objects(self):
-+        # group override for user group should fail
-+        result = self.master.run_command(
-+            ['ipa', 'idoverridegroup-add', self.idview, self.user1,
-+             '--gid', str(self.user1_gid_override)],
-+            raiseonerr=False
-+        )
-+        assert result.returncode == 1
-+        assert "cannot be overridden" in result.stderr_text
-+
-+    def test_anchor_groupname(self):
-+        result = self.master.run_command([
-+            'ipa', 'idoverridegroup-find', self.idview,
-+            '--anchor', self.group1
-+        ])
-+        expected = "Anchor to override: {}".format(self.group1)
-+        assert expected in result.stdout_text
-
-ONLY APPLYING TO ipatests/prci_definitions/nightly_rawhide.yaml, other
-files are not available or compatible
-
-From e86498ea2f8259118025e622cc5f1cf2c26f2757 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Dec 07 2018 15:06:13 +0000
-Subject: Run idviews integration tests in nightly
-
-
-See: https://pagure.io/freeipa/issue/6594
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
-
----
-
-#diff --git a/ipatests/prci_definitions/nightly_f28.yaml b/ipatests/prci_definitions/nightly_f28.yaml
-#index 8462c14..ac792f1 100644
-#--- a/ipatests/prci_definitions/nightly_f28.yaml
-#+++ b/ipatests/prci_definitions/nightly_f28.yaml
-#@@ -195,6 +195,18 @@ jobs:
-#         timeout: 10800
-#         topology: *master_1repl
-# 
-#+  fedora-28/test_idviews:
-#+    requires: [fedora-28/build]
-#+    priority: 50
-#+    job:
-#+      class: RunPytest
-#+      args:
-#+        build_url: '{fedora-28/build_url}'
-#+        test_suite: test_integration/test_idviews.py::TestIDViews
-#+        template: *ci-master-f28
-#+        timeout: 3600
-#+        topology: *master_1repl_1client
-#+
-#   fedora-28/test_caless_TestServerInstall:
-#     requires: [fedora-28/build]
-#     priority: 50
-#diff --git a/ipatests/prci_definitions/nightly_master.yaml b/ipatests/prci_definitions/nightly_master.yaml
-#index 3f2b346..953a60e 100644
-#--- a/ipatests/prci_definitions/nightly_master.yaml
-#+++ b/ipatests/prci_definitions/nightly_master.yaml
-#@@ -195,6 +195,18 @@ jobs:
-#         timeout: 10800
-#         topology: *master_1repl
-# 
-#+  fedora-28/test_idviews:
-#+    requires: [fedora-29/build]
-#+    priority: 50
-#+    job:
-#+      class: RunPytest
-#+      args:
-#+        build_url: '{fedora-29/build_url}'
-#+        test_suite: test_integration/test_idviews.py::TestIDViews
-#+        template: *ci-master-f29
-#+        timeout: 3600
-#+        topology: *master_1repl_1client
-#+
-#   fedora-29/test_caless_TestServerInstall:
-#     requires: [fedora-29/build]
-#     priority: 50
-diff --git a/ipatests/prci_definitions/nightly_rawhide.yaml b/ipatests/prci_definitions/nightly_rawhide.yaml
-index bdc34d2..e74e5f6 100644
---- a/ipatests/prci_definitions/nightly_rawhide.yaml
-+++ b/ipatests/prci_definitions/nightly_rawhide.yaml
-@@ -195,6 +195,18 @@ jobs:
-         timeout: 10800
-         topology: *master_1repl
- 
-+  fedora-28/test_idviews:
-+    requires: [fedora-rawhide/build]
-+    priority: 50
-+    job:
-+      class: RunPytest
-+      args:
-+        build_url: '{fedora-rawhide/build_url}'
-+        test_suite: test_integration/test_idviews.py::TestIDViews
-+        template: *ci-master-frawhide
-+        timeout: 3600
-+        topology: *master_1repl_1client
-+
-   fedora-rawhide/test_caless_TestServerInstall:
-     requires: [fedora-rawhide/build]
-     priority: 50
-
diff --git a/SOURCES/0030-Fix-systemd-user-HBAC-rule.patch b/SOURCES/0030-Fix-systemd-user-HBAC-rule.patch
deleted file mode 100644
index 50fe287..0000000
--- a/SOURCES/0030-Fix-systemd-user-HBAC-rule.patch
+++ /dev/null
@@ -1,58 +0,0 @@
-From b3f06994b7b44a0f9cd0c6bd0302c9db87dc2502 Mon Sep 17 00:00:00 2001
-From: Christian Heimes <cheimes@redhat.com>
-Date: Tue, 15 Jan 2019 17:33:56 +0100
-Subject: [PATCH] Fix systemd-user HBAC rule
-
-2ef6e14c5a87724a3b37dd5f0817af48c4411e03 added an invalid HBAC rule that
-encoded the service wrongly.
-
-See: https://bugzilla.redhat.com/show_bug.cgi?id=1643928
-Fixes: https://pagure.io/freeipa/issue/7831
-Signed-off-by: Christian Heimes <cheimes@redhat.com>
----
- install/share/default-hbac.ldif            |  2 +-
- ipatests/test_integration/test_commands.py | 12 +++++++++---
- 2 files changed, 10 insertions(+), 4 deletions(-)
-
-diff --git a/install/share/default-hbac.ldif b/install/share/default-hbac.ldif
-index 8dd90685c..c89bd3eef 100644
---- a/install/share/default-hbac.ldif
-+++ b/install/share/default-hbac.ldif
-@@ -21,7 +21,7 @@ cn: allow_systemd-user
- accessruletype: allow
- usercategory: all
- hostcategory: all
--servicecategory: systemd-user
-+memberService: cn=systemd-user,cn=hbacservices,cn=hbac,$SUFFIX
- ipaenabledflag: TRUE
- description: Allow pam_systemd to run user@.service to create a system user session
- ipauniqueid: autogenerate
-diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py
-index 1fb6450a2..8b2c84fc6 100644
---- a/ipatests/test_integration/test_commands.py
-+++ b/ipatests/test_integration/test_commands.py
-@@ -500,12 +500,18 @@ class TestIPACommand(IntegrationTest):
-         # https://pagure.io/freeipa/issue/7831
-         tasks.kinit_admin(self.master)
-         # check for presence
--        self.master.run_command(
--            ['ipa', 'hbacrule-show', 'allow_systemd-user']
--        )
-         self.master.run_command(
-             ['ipa', 'hbacsvc-show', 'systemd-user']
-         )
-+        result = self.master.run_command(
-+            ['ipa', 'hbacrule-show', 'allow_systemd-user', '--all']
-+        )
-+        lines = set(l.strip() for l in result.stdout_text.split('\n'))
-+        assert 'User category: all' in lines
-+        assert 'Host category: all' in lines
-+        assert 'Enabled: TRUE' in lines
-+        assert 'Services: systemd-user' in lines
-+        assert 'accessruletype: allow' in lines
- 
-         # delete both
-         self.master.run_command(
--- 
-2.20.1
-
diff --git a/SOURCES/0031-ipa-client-automount-handle-NFS-configuration-file-c.patch b/SOURCES/0031-ipa-client-automount-handle-NFS-configuration-file-c.patch
deleted file mode 100644
index 04ddf59..0000000
--- a/SOURCES/0031-ipa-client-automount-handle-NFS-configuration-file-c.patch
+++ /dev/null
@@ -1,189 +0,0 @@
-From c69875c8afdd877baf7139c0cd5241f70105cbd4 Mon Sep 17 00:00:00 2001
-From: =?UTF-8?q?Fran=C3=A7ois=20Cami?= <fcami@redhat.com>
-Date: Tue, 26 Feb 2019 13:59:06 +0100
-Subject: [PATCH] ipa-client-automount: handle NFS configuration file changes
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-nfs-utils in Fedora 30 and later switched its configuration
-file from /etc/sysconfig/nfs to /etc/nfs.conf, providing a
-conversion service (nfs-convert.service) for upgrades.
-However, for new installs the original configuration file
-is missing. This change:
-* adds a tuple-based osinfo.version_number method to handle
-  more kinds of OS versioning schemes
-* detects RHEL and Fedora versions with the the new nfs-utils
-  behavior
-* avoids backing up the new NFS configuration file as we do
-  not have to modify it.
-
-See: https://bugzilla.redhat.com/show_bug.cgi?id=1676981
-
-Fixes: https://pagure.io/freeipa/issue/7868
-Signed-off-by: François Cami <fcami@redhat.com>
-Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
-Reviewed-By: Christian Heimes <cheimes@redhat.com>
-Reviewed-By: Rob Crittenden <rcritten@redhat.com>
----
- client/ipa-client-automount.in  | 18 ++++++++++--------
- ipaplatform/fedora/constants.py |  9 ++++++++-
- ipaplatform/fedora/paths.py     |  3 +++
- ipaplatform/fedora/services.py  |  2 +-
- ipaplatform/osinfo.py           |  9 +++++++++
- ipaplatform/rhel/constants.py   |  7 +++++++
- ipaplatform/rhel/paths.py       |  4 +++-
- 7 files changed, 41 insertions(+), 11 deletions(-)
-
-diff --git a/client/ipa-client-automount.in b/client/ipa-client-automount.in
-index 15926bd..f9eda9c 100755
---- a/client/ipa-client-automount.in
-+++ b/client/ipa-client-automount.in
-@@ -335,14 +335,16 @@ def configure_nfs(fstore, statestore):
-     """
-     Configure secure NFS
-     """
--    replacevars = {
--        constants.SECURE_NFS_VAR: 'yes',
--    }
--    ipautil.backup_config_and_replace_variables(fstore,
--        paths.SYSCONFIG_NFS, replacevars=replacevars)
--    tasks.restore_context(paths.SYSCONFIG_NFS)
--
--    print("Configured %s" % paths.SYSCONFIG_NFS)
-+    # Newer Fedora releases ship /etc/nfs.conf instead of /etc/sysconfig/nfs
-+    # and do not require changes there. On these, SECURE_NFS_VAR == None
-+    if constants.SECURE_NFS_VAR:
-+        replacevars = {
-+            constants.SECURE_NFS_VAR: 'yes',
-+        }
-+        ipautil.backup_config_and_replace_variables(fstore,
-+            paths.SYSCONFIG_NFS, replacevars=replacevars)
-+        tasks.restore_context(paths.SYSCONFIG_NFS)
-+        print("Configured %s" % paths.SYSCONFIG_NFS)
- 
-     # Prepare the changes
-     # We need to use IPAChangeConf as simple regexp substitution
-diff --git a/ipaplatform/fedora/constants.py b/ipaplatform/fedora/constants.py
-index d48696e..744b30a 100644
---- a/ipaplatform/fedora/constants.py
-+++ b/ipaplatform/fedora/constants.py
-@@ -10,6 +10,12 @@ This Fedora base platform module exports platform related constants.
- from __future__ import absolute_import
- 
- from ipaplatform.redhat.constants import RedHatConstantsNamespace
-+from ipaplatform.osinfo import osinfo
-+
-+# Fedora 28 and earlier use /etc/sysconfig/nfs
-+# Fedora 30 and later use /etc/nfs.conf
-+# Fedora 29 has both
-+HAS_NFS_CONF = osinfo.version_number >= (30,)
- 
- 
- class FedoraConstantsNamespace(RedHatConstantsNamespace):
-@@ -22,6 +28,7 @@ class FedoraConstantsNamespace(RedHatConstantsNamespace):
-     # secure remote password, and DSA cert authentication.
-     # see https://fedoraproject.org/wiki/Changes/CryptoPolicy
-     TLS_HIGH_CIPHERS = "PROFILE=SYSTEM:!3DES:!PSK:!SRP:!aDSS"
--
-+    if HAS_NFS_CONF:
-+        SECURE_NFS_VAR = None
- 
- constants = FedoraConstantsNamespace()
-diff --git a/ipaplatform/fedora/paths.py b/ipaplatform/fedora/paths.py
-index a9bdedf..4e993c0 100644
---- a/ipaplatform/fedora/paths.py
-+++ b/ipaplatform/fedora/paths.py
-@@ -26,6 +26,7 @@ in Fedora-based systems.
- from __future__ import absolute_import
- 
- from ipaplatform.redhat.paths import RedHatPathNamespace
-+from ipaplatform.fedora.constants import HAS_NFS_CONF
- 
- 
- class FedoraPathNamespace(RedHatPathNamespace):
-@@ -33,6 +34,8 @@ class FedoraPathNamespace(RedHatPathNamespace):
-         "/etc/httpd/conf.modules.d/02-ipa-wsgi.conf"
-     )
-     NAMED_CRYPTO_POLICY_FILE = "/etc/crypto-policies/back-ends/bind.config"
-+    if HAS_NFS_CONF:
-+        SYSCONFIG_NFS = '/etc/nfs.conf'
- 
- 
- paths = FedoraPathNamespace()
-diff --git a/ipaplatform/fedora/services.py b/ipaplatform/fedora/services.py
-index 5ff64f1..543cb1b 100644
---- a/ipaplatform/fedora/services.py
-+++ b/ipaplatform/fedora/services.py
-@@ -34,7 +34,7 @@ fedora_system_units = redhat_services.redhat_system_units.copy()
- # Fedora 28 and earlier have fedora-domainname.service. Starting from
- # Fedora 29, the service is called nis-domainname.service as defined in
- # ipaplatform.redhat.services.
--HAS_FEDORA_DOMAINNAME_SERVICE = int(osinfo.version_id) <= 28
-+HAS_FEDORA_DOMAINNAME_SERVICE = osinfo.version_number <= (28,)
- 
- if HAS_FEDORA_DOMAINNAME_SERVICE:
-     fedora_system_units['domainname'] = 'fedora-domainname.service'
-diff --git a/ipaplatform/osinfo.py b/ipaplatform/osinfo.py
-index a38165d..35b024e 100644
---- a/ipaplatform/osinfo.py
-+++ b/ipaplatform/osinfo.py
-@@ -178,6 +178,15 @@ class OSInfo(Mapping):
-         return self._info.get('VERSION_ID')
- 
-     @property
-+    def version_number(self):
-+        """Version number tuple based on version_id
-+        """
-+        version_id = self._info.get('VERSION_ID')
-+        if not version_id:
-+            return ()
-+        return tuple(int(p) for p in version_id.split('.'))
-+
-+    @property
-     def platform_ids(self):
-         """Ordered tuple of detected platforms (including override)
-         """
-diff --git a/ipaplatform/rhel/constants.py b/ipaplatform/rhel/constants.py
-index 72335ac..073e332 100644
---- a/ipaplatform/rhel/constants.py
-+++ b/ipaplatform/rhel/constants.py
-@@ -10,10 +10,17 @@ This RHEL base platform module exports platform related constants.
- from __future__ import absolute_import
- 
- from ipaplatform.redhat.constants import RedHatConstantsNamespace
-+from ipaplatform.osinfo import osinfo
-+
-+# RHEL 7 and earlier use /etc/sysconfig/nfs
-+# RHEL 8 uses /etc/nfs.conf
-+HAS_NFS_CONF = osinfo.version_number >= (8,)
- 
- 
- class RHELConstantsNamespace(RedHatConstantsNamespace):
-     IPA_ADTRUST_PACKAGE_NAME = "ipa-server-trust-ad"
-     IPA_DNS_PACKAGE_NAME = "ipa-server-dns"
-+    if HAS_NFS_CONF:
-+        SECURE_NFS_VAR = None
- 
- constants = RHELConstantsNamespace()
-diff --git a/ipaplatform/rhel/paths.py b/ipaplatform/rhel/paths.py
-index d8b64ab..c081ada 100644
---- a/ipaplatform/rhel/paths.py
-+++ b/ipaplatform/rhel/paths.py
-@@ -26,10 +26,12 @@ in RHEL-based systems.
- from __future__ import absolute_import
- 
- from ipaplatform.redhat.paths import RedHatPathNamespace
-+from ipaplatform.rhel.constants import HAS_NFS_CONF
- 
- 
- class RHELPathNamespace(RedHatPathNamespace):
--    pass
-+    if HAS_NFS_CONF:
-+        SYSCONFIG_NFS = '/etc/nfs.conf'
- 
- 
- paths = RHELPathNamespace()
--- 
-2.9.3
-
diff --git a/SOURCES/1002-4.8.0-Remove-csrgen.patch b/SOURCES/1002-4.8.0-Remove-csrgen.patch
new file mode 100644
index 0000000..8b7e374
--- /dev/null
+++ b/SOURCES/1002-4.8.0-Remove-csrgen.patch
@@ -0,0 +1,2051 @@
+Addtional patches that need to be partly reverted that are touching csrgen
+related files:
+
+7b8a2af2197381058ca532d1ae206defb16fac88
+ac6568dcf58ec8d06df5493d14a28aa41845d4ef
+9c86d35a3f0af4a793fada7dfe726e9cc66782ea
+9836511a2b6d7cf48b1a54cb3158e5eac674081a
+b431e9b684df11c811892bd9d2a5711355f0076e
+
+This is a collection of an existing patch to remove csrgen for 4.7.1 and
+additional patches that have been added for 4.7.90 pre1.
+
+Additional reverted csrgen patches:
+
+852618fd6529fbdd7b03077fae37c6fbbe45b51b
+0ac1d3ea62efd9751fcc59cea46bcdafe1f11c37
+7633d62d858c14523a99143aa0ff36f76bb4ff68
+53f87ee5cd9d19f6fb91a9a1eafc8ea798095954
+395a68d20887d0ac010e480e68b225d6dfeff726
+03786ad9f3bd5edc351040847b8a49c9cd9288b2
+c9d710a446d10aad72795e15bf041b87102628c1
+2b90c8a20e45ade9bfd27731cccc94a34cf3f61e
+61dde27f70b9f8dd1b57ad1fbc3744f3c380613a
+806784dbd9e69a89c7a705c89bf42ba1fd4265c9
+79378c90512a1cdd5f3d5ec6482e434caea06e01
+bd5a5012d24820b54cdca2955f5405b84de1178c
+26ab51ddf47f421f3404709052db89f08c05adaa
+a53e17830c3d4fd59a62248d4447491675c6a80e
+e7588ab2dc73e7f66ebc6cdcfb99470540e37731
+136c6c3e2a4f77a27f435efd4a1cd95c9e089314
+5420e9cfbe7803808b6e26d2dae64f2a6a50149a
+
+Original patch from 4.7.1:
+
+From 468bcf90cb985e2b1eb394bd752dc39aa4b75582 Mon Sep 17 00:00:00 2001
+From: Rob Crittenden <rcritten@redhat.com>
+Date: Thu, 19 Jul 2018 18:37:18 -0400
+Subject: [PATCH] Remove csrgen
+
+This reverts commits:
+* 72de679eb445c975ec70cd265d37d4927823ce5b
+* 177f07e163d6d591a1e609d35e0a6f6f5347551e
+* 80be18162921268be9c8981495c9e8a4de0c85cd
+* 83e2c2b65eeb5a3aa4a59c0535e9177aac5e4637
+* ada91c20588046bb147fc701718d3da4d2c080ca
+* 4350dcdea22fd2284836315d0ae7d38733a7620e
+* 39a5d9c5aae77687f67d9be02457733bdfb99ead
+* a26cf0d7910dd4c0a4da08682b4be8d3d94ba520
+* afd7c05d11432304bfdf183832a21d419f363689
+* f1a1c6eca1b294f24174d7b0e1f78de46d9d5b05
+* fc58eff6a3d7fe805e612b8b002304d8b9cd4ba9
+* 10ef5947860f5098182b1f95c08c1158e2da15f9
+
+https://bugzilla.redhat.com/show_bug.cgi?id=1432630
+---
+ freeipa.spec.in                                    |  14 -
+ ipaclient/csrgen.py                                | 488 ---------------------
+ ipaclient/csrgen/profiles/caIPAserviceCert.json    |  15 -
+ ipaclient/csrgen/profiles/userCert.json            |  15 -
+ ipaclient/csrgen/rules/dataDNS.json                |   8 -
+ ipaclient/csrgen/rules/dataEmail.json              |   8 -
+ ipaclient/csrgen/rules/dataHostCN.json             |   8 -
+ ipaclient/csrgen/rules/dataSubjectBase.json        |   8 -
+ ipaclient/csrgen/rules/dataUsernameCN.json         |   8 -
+ ipaclient/csrgen/rules/syntaxSAN.json              |   8 -
+ ipaclient/csrgen/rules/syntaxSubject.json          |   9 -
+ ipaclient/csrgen/templates/openssl_base.tmpl       |  17 -
+ ipaclient/csrgen/templates/openssl_macros.tmpl     |  29 --
+ ipaclient/csrgen_ffi.py                            | 331 --------------
+ ipaclient/plugins/cert.py                          |  80 ----
+ ipaclient/plugins/csrgen.py                        | 128 ------
+ ipaclient/setup.py                                 |   8 -
+ .../data/test_csrgen/configs/caIPAserviceCert.conf |  16 -
+ .../data/test_csrgen/configs/userCert.conf         |  16 -
+ .../data/test_csrgen/profiles/profile.json         |   8 -
+ .../data/test_csrgen/rules/basic.json              |   5 -
+ .../data/test_csrgen/rules/options.json            |   8 -
+ .../data/test_csrgen/templates/identity_base.tmpl  |   1 -
+ ipatests/test_ipaclient/test_csrgen.py             | 304 -------------
+ 24 files changed, 1540 deletions(-)
+ delete mode 100644 ipaclient/csrgen.py
+ delete mode 100644 ipaclient/csrgen/profiles/caIPAserviceCert.json
+ delete mode 100644 ipaclient/csrgen/profiles/userCert.json
+ delete mode 100644 ipaclient/csrgen/rules/dataDNS.json
+ delete mode 100644 ipaclient/csrgen/rules/dataEmail.json
+ delete mode 100644 ipaclient/csrgen/rules/dataHostCN.json
+ delete mode 100644 ipaclient/csrgen/rules/dataSubjectBase.json
+ delete mode 100644 ipaclient/csrgen/rules/dataUsernameCN.json
+ delete mode 100644 ipaclient/csrgen/rules/syntaxSAN.json
+ delete mode 100644 ipaclient/csrgen/rules/syntaxSubject.json
+ delete mode 100644 ipaclient/csrgen/templates/openssl_base.tmpl
+ delete mode 100644 ipaclient/csrgen/templates/openssl_macros.tmpl
+ delete mode 100644 ipaclient/csrgen_ffi.py
+ delete mode 100644 ipaclient/plugins/csrgen.py
+ delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
+ delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
+ delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json
+ delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/rules/basic.json
+ delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/rules/options.json
+ delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl
+ delete mode 100644 ipatests/test_ipaclient/test_csrgen.py
+
+diff -urN freeipa-4.8.0/freeipa.spec.in freeipa-4.8.0.removed_csrgen/freeipa.spec.in
+--- freeipa-4.8.0/freeipa.spec.in	2019-06-29 10:01:30.458735813 +0200
++++ freeipa-4.8.0.removed_csrgen/freeipa.spec.in	2019-07-03 13:24:38.471222723 +0200
+@@ -1247,13 +1247,6 @@
+ %dir %{python3_sitelib}/ipaclient/remote_plugins/2_*
+ %{python3_sitelib}/ipaclient/remote_plugins/2_*/*.py
+ %{python3_sitelib}/ipaclient/remote_plugins/2_*/__pycache__/*.py*
+-%dir %{python3_sitelib}/ipaclient/csrgen
+-%dir %{python3_sitelib}/ipaclient/csrgen/profiles
+-%{python3_sitelib}/ipaclient/csrgen/profiles/*.json
+-%dir %{python3_sitelib}/ipaclient/csrgen/rules
+-%{python3_sitelib}/ipaclient/csrgen/rules/*.json
+-%dir %{python3_sitelib}/ipaclient/csrgen/templates
+-%{python3_sitelib}/ipaclient/csrgen/templates/*.tmpl
+ %{python3_sitelib}/ipaclient-*.egg-info
+ 
+ 
+diff -urN freeipa-4.8.0/ipaclient/csrgen/profiles/caIPAserviceCert.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/profiles/caIPAserviceCert.json
+--- freeipa-4.8.0/ipaclient/csrgen/profiles/caIPAserviceCert.json	2019-07-03 08:42:41.844539797 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/profiles/caIPAserviceCert.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,15 +0,0 @@
+-[
+-    {
+-        "syntax": "syntaxSubject",
+-        "data": [
+-            "dataHostCN",
+-            "dataSubjectBase"
+-        ]
+-    },
+-    {
+-        "syntax": "syntaxSAN",
+-        "data": [
+-            "dataDNS"
+-        ]
+-    }
+-]
+diff -urN freeipa-4.8.0/ipaclient/csrgen/profiles/userCert.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/profiles/userCert.json
+--- freeipa-4.8.0/ipaclient/csrgen/profiles/userCert.json	2019-07-03 08:42:41.848539737 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/profiles/userCert.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,15 +0,0 @@
+-[
+-    {
+-        "syntax": "syntaxSubject",
+-        "data": [
+-            "dataUsernameCN",
+-            "dataSubjectBase"
+-        ]
+-    },
+-    {
+-        "syntax": "syntaxSAN",
+-        "data": [
+-            "dataEmail"
+-        ]
+-    }
+-]
+diff -urN freeipa-4.8.0/ipaclient/csrgen/rules/dataDNS.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataDNS.json
+--- freeipa-4.8.0/ipaclient/csrgen/rules/dataDNS.json	2019-07-03 08:42:41.853539663 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataDNS.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,8 +0,0 @@
+-{
+-  "rule": {
+-    "template": "DNS = {{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}"
+-  },
+-  "options": {
+-    "data_source": "subject.krbprincipalname.0.partition('/')[2].partition('@')[0]"
+-  }
+-}
+diff -urN freeipa-4.8.0/ipaclient/csrgen/rules/dataEmail.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataEmail.json
+--- freeipa-4.8.0/ipaclient/csrgen/rules/dataEmail.json	2019-07-03 08:42:41.857539603 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataEmail.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,8 +0,0 @@
+-{
+-  "rule": {
+-    "template": "email = {{subject.mail.0}}"
+-  },
+-  "options": {
+-    "data_source": "subject.mail.0"
+-  }
+-}
+diff -urN freeipa-4.8.0/ipaclient/csrgen/rules/dataHostCN.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataHostCN.json
+--- freeipa-4.8.0/ipaclient/csrgen/rules/dataHostCN.json	2019-07-03 08:42:41.861539544 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataHostCN.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,8 +0,0 @@
+-{
+-  "rule": {
+-    "template": "CN={{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}"
+-  },
+-  "options": {
+-    "data_source": "subject.krbprincipalname.0.partition('/')[2].partition('@')[0]"
+-  }
+-}
+diff -urN freeipa-4.8.0/ipaclient/csrgen/rules/dataSubjectBase.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataSubjectBase.json
+--- freeipa-4.8.0/ipaclient/csrgen/rules/dataSubjectBase.json	2019-07-03 08:42:41.865539484 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataSubjectBase.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,8 +0,0 @@
+-{
+-  "rule": {
+-    "template": "{{config.ipacertificatesubjectbase.0}}"
+-  },
+-  "options": {
+-    "data_source": "config.ipacertificatesubjectbase.0"
+-  }
+-}
+diff -urN freeipa-4.8.0/ipaclient/csrgen/rules/dataUsernameCN.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataUsernameCN.json
+--- freeipa-4.8.0/ipaclient/csrgen/rules/dataUsernameCN.json	2019-07-03 08:42:41.869539424 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/dataUsernameCN.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,8 +0,0 @@
+-{
+-  "rule": {
+-    "template": "CN={{subject.uid.0}}"
+-  },
+-  "options": {
+-    "data_source": "subject.uid.0"
+-  }
+-}
+diff -urN freeipa-4.8.0/ipaclient/csrgen/rules/syntaxSAN.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/syntaxSAN.json
+--- freeipa-4.8.0/ipaclient/csrgen/rules/syntaxSAN.json	2019-07-03 08:42:41.874539350 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/syntaxSAN.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,8 +0,0 @@
+-{
+-  "rule": {
+-    "template": "subjectAltName = @{% call openssl.section() %}{{ datarules|join('\n') }}{% endcall %}"
+-  },
+-  "options": {
+-    "extension": true
+-  }
+-}
+diff -urN freeipa-4.8.0/ipaclient/csrgen/rules/syntaxSubject.json freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/syntaxSubject.json
+--- freeipa-4.8.0/ipaclient/csrgen/rules/syntaxSubject.json	2019-07-03 08:42:41.878539290 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/rules/syntaxSubject.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,9 +0,0 @@
+-{
+-  "rule": {
+-    "template": "distinguished_name = {% call openssl.section() %}{{ datarules|reverse|join('\n') }}{% endcall %}"
+-  },
+-  "options": {
+-    "required": true,
+-    "data_source_combinator": "and"
+-  }
+-}
+diff -urN freeipa-4.8.0/ipaclient/csrgen/templates/openssl_base.tmpl freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/templates/openssl_base.tmpl
+--- freeipa-4.8.0/ipaclient/csrgen/templates/openssl_base.tmpl	2019-07-03 08:42:41.882539231 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/templates/openssl_base.tmpl	1970-01-01 01:00:00.000000000 +0100
+@@ -1,17 +0,0 @@
+-{% raw -%}
+-{% import "openssl_macros.tmpl" as openssl -%}
+-{% endraw -%}
+-[ req ]
+-prompt = no
+-encrypt_key = no
+-
+-{{ parameters|join('\n') }}
+-{% raw %}{% set rendered_extensions -%}{% endraw %}
+-{{ extensions|join('\n') }}
+-{% raw -%}
+-{%- endset -%}
+-{% if rendered_extensions -%}
+-req_extensions = {% call openssl.section() %}{{ rendered_extensions }}{% endcall %}
+-{% endif %}
+-{{ openssl.openssl_sections|join('\n\n') }}
+-{%- endraw %}
+diff -urN freeipa-4.8.0/ipaclient/csrgen/templates/openssl_macros.tmpl freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/templates/openssl_macros.tmpl
+--- freeipa-4.8.0/ipaclient/csrgen/templates/openssl_macros.tmpl	2019-07-03 08:42:41.886539171 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen/templates/openssl_macros.tmpl	1970-01-01 01:00:00.000000000 +0100
+@@ -1,29 +0,0 @@
+-{# List containing rendered sections to be included at end #}
+-{% set openssl_sections = [] %}
+-
+-{#
+-List containing one entry for each section name allocated. Because of
+-scoping rules, we need to use a list so that it can be a "per-render global"
+-that gets updated in place. Real globals are shared by all templates with the
+-same environment, and variables defined in the macro don't persist after the
+-macro invocation ends.
+-#}
+-{% set openssl_section_num = [] %}
+-
+-{% macro section() -%}
+-{% set name -%}
+-sec{{ openssl_section_num|length -}}
+-{% endset -%}
+-{% do openssl_section_num.append('') -%}
+-{% set contents %}{{ caller() }}{% endset -%}
+-{% if contents -%}
+-{% set sectiondata = formatsection(name, contents) -%}
+-{% do openssl_sections.append(sectiondata) -%}
+-{% endif -%}
+-{{ name -}}
+-{% endmacro %}
+-
+-{% macro formatsection(name, contents) -%}
+-[ {{ name }} ]
+-{{ contents -}}
+-{% endmacro %}
+diff -urN freeipa-4.8.0/ipaclient/csrgen_ffi.py freeipa-4.8.0.removed_csrgen/ipaclient/csrgen_ffi.py
+--- freeipa-4.8.0/ipaclient/csrgen_ffi.py	2019-07-03 08:42:41.816540214 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen_ffi.py	1970-01-01 01:00:00.000000000 +0100
+@@ -1,387 +0,0 @@
+-from cffi import FFI
+-import ctypes.util
+-
+-from ipalib import errors
+-
+-_ffi = FFI()
+-
+-_ffi.cdef('''
+-/* libcrypto/crypto.h */
+-unsigned long OpenSSL_version_num(void);
+-unsigned long SSLeay(void);
+-const char * OpenSSL_version(int t);
+-const char * SSLeay_version(int t);
+-
+-#define OPENSSL_VERSION 0
+-''')
+-
+-_libcrypto = _ffi.dlopen(ctypes.util.find_library('crypto'))
+-
+-# SSLeay_version has been renamed with OpenSSL_version in OpenSSL 1.1.0
+-# LibreSSL has OpenSSL_version since 2.7.0
+-try:
+-    OpenSSL_version = _libcrypto.OpenSSL_version
+-except AttributeError:
+-    OpenSSL_version = _libcrypto.SSLeay_version
+-
+-_version = OpenSSL_version(_libcrypto.OPENSSL_VERSION)
+-_version = _ffi.string(_version).decode('utf-8')
+-LIBRESSL = _version.startswith('LibreSSL')
+-if not _version.startswith("OpenSSL") and not LIBRESSL:
+-    raise ImportError("Only LibreSSL and OpenSSL are supported")
+-
+-# SSLeay has been renamed with OpenSSL_version_num in OpenSSL 1.1.0
+-# LibreSSL has OpenSSL_version_num since 2.7.0
+-try:
+-    OpenSSL_version_num = _libcrypto.OpenSSL_version_num
+-except AttributeError:
+-    OpenSSL_version_num = _libcrypto.SSLeay
+-
+-# OpenSSL_version_num()/SSLeay() returns the value of OPENSSL_VERSION_NUMBER
+-#
+-# OPENSSL_VERSION_NUMBER is a numeric release version identifier:
+-# MNNFFPPS: major minor fix patch status
+-# For example,
+-# 0x000906000 == 0.9.6 dev
+-# 0x000906023 == 0.9.6b beta 3
+-# 0x00090605f == 0.9.6e release
+-_openssl_version = OpenSSL_version_num()
+-
+-_ffi.cdef('''
+-typedef ... CONF;
+-typedef ... CONF_METHOD;
+-typedef ... BIO;
+-typedef ... ipa_STACK_OF_CONF_VALUE;
+-
+-/* openssl/conf.h */
+-typedef struct {
+-    char *section;
+-    char *name;
+-    char *value;
+-} CONF_VALUE;
+-
+-CONF *NCONF_new(CONF_METHOD *meth);
+-void NCONF_free(CONF *conf);
+-int NCONF_load_bio(CONF *conf, BIO *bp, long *eline);
+-ipa_STACK_OF_CONF_VALUE *NCONF_get_section(const CONF *conf,
+-                                        const char *section);
+-char *NCONF_get_string(const CONF *conf, const char *group, const char *name);
+-
+-/* openssl/safestack.h */
+-// int sk_CONF_VALUE_num(ipa_STACK_OF_CONF_VALUE *);
+-// CONF_VALUE *sk_CONF_VALUE_value(ipa_STACK_OF_CONF_VALUE *, int);
+-
+-/* openssl/stack.h */
+-typedef ... _STACK;
+-
+-int OPENSSL_sk_num(const _STACK *);
+-void *OPENSSL_sk_value(const _STACK *, int);
+-
+-int sk_num(const _STACK *);
+-void *sk_value(const _STACK *, int);
+-
+-/* openssl/bio.h */
+-BIO *BIO_new_mem_buf(const void *buf, int len);
+-int BIO_free(BIO *a);
+-
+-/* openssl/asn1.h */
+-typedef struct ASN1_ENCODING_st {
+-    unsigned char *enc;         /* DER encoding */
+-    long len;                   /* Length of encoding */
+-    int modified;               /* set to 1 if 'enc' is invalid */
+-} ASN1_ENCODING;
+-
+-/* openssl/evp.h */
+-typedef ... EVP_PKEY;
+-
+-void EVP_PKEY_free(EVP_PKEY *pkey);
+-
+-/* openssl/x509.h */
+-typedef ... ASN1_INTEGER;
+-typedef ... ASN1_BIT_STRING;
+-typedef ... ASN1_OBJECT;
+-typedef ... X509;
+-typedef ... X509_CRL;
+-typedef ... X509_NAME;
+-typedef ... X509_PUBKEY;
+-typedef ... ipa_STACK_OF_X509_ATTRIBUTE;
+-
+-typedef struct X509_req_info_st {
+-    ASN1_ENCODING enc;
+-    ASN1_INTEGER *version;
+-    X509_NAME *subject;
+-    X509_PUBKEY *pubkey;
+-    /*  d=2 hl=2 l=  0 cons: cont: 00 */
+-    ipa_STACK_OF_X509_ATTRIBUTE *attributes; /* [ 0 ] */
+-} X509_REQ_INFO;
+-''')
+-
+-# since OpenSSL 1.1.0 req_info field is no longer pointer to X509_REQ_INFO
+-if _openssl_version >= 0x10100000 and not LIBRESSL:
+-    _ffi.cdef('''
+-    typedef struct X509_req_st {
+-        X509_REQ_INFO req_info;
+-    } X509_REQ;
+-    ''')
+-else:
+-    _ffi.cdef('''
+-    typedef struct X509_req_st {
+-        X509_REQ_INFO *req_info;
+-    } X509_REQ;
+-    ''')
+-
+-_ffi.cdef('''
+-X509_REQ *X509_REQ_new(void);
+-void X509_REQ_free(X509_REQ *);
+-EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a);
+-int X509_REQ_set_pubkey(X509_REQ *x, EVP_PKEY *pkey);
+-int X509_NAME_add_entry_by_OBJ(X509_NAME *name, const ASN1_OBJECT *obj, int type,
+-                               const unsigned char *bytes, int len, int loc,
+-                               int set);
+-int X509_NAME_entry_count(X509_NAME *name);
+-int i2d_X509_REQ_INFO(X509_REQ_INFO *a, unsigned char **out);
+-
+-/* openssl/objects.h */
+-ASN1_OBJECT *OBJ_txt2obj(const char *s, int no_name);
+-
+-/* openssl/x509v3.h */
+-typedef ... X509V3_CONF_METHOD;
+-
+-typedef struct v3_ext_ctx {
+-    int flags;
+-    X509 *issuer_cert;
+-    X509 *subject_cert;
+-    X509_REQ *subject_req;
+-    X509_CRL *crl;
+-    X509V3_CONF_METHOD *db_meth;
+-    void *db;
+-} X509V3_CTX;
+-
+-void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject,
+-                    X509_REQ *req, X509_CRL *crl, int flags);
+-void X509V3_set_nconf(X509V3_CTX *ctx, CONF *conf);
+-int X509V3_EXT_REQ_add_nconf(CONF *conf, X509V3_CTX *ctx, char *section,
+-                             X509_REQ *req);
+-
+-/* openssl/x509v3.h */
+-unsigned long ERR_get_error(void);
+-char *ERR_error_string(unsigned long e, char *buf);
+-''')  # noqa: E501
+-
+-NULL = _ffi.NULL
+-# openssl/conf.h
+-NCONF_new = _libcrypto.NCONF_new
+-NCONF_free = _libcrypto.NCONF_free
+-NCONF_load_bio = _libcrypto.NCONF_load_bio
+-NCONF_get_section = _libcrypto.NCONF_get_section
+-NCONF_get_string = _libcrypto.NCONF_get_string
+-
+-# openssl/stack.h
+-try:
+-    sk_num = _libcrypto.OPENSSL_sk_num
+-    sk_value = _libcrypto.OPENSSL_sk_value
+-except AttributeError:
+-    sk_num = _libcrypto.sk_num
+-    sk_value = _libcrypto.sk_value
+-
+-
+-def sk_CONF_VALUE_num(sk):
+-    return sk_num(_ffi.cast("_STACK *", sk))
+-
+-
+-def sk_CONF_VALUE_value(sk, i):
+-    return _ffi.cast("CONF_VALUE *", sk_value(_ffi.cast("_STACK *", sk), i))
+-
+-
+-# openssl/bio.h
+-BIO_new_mem_buf = _libcrypto.BIO_new_mem_buf
+-BIO_free = _libcrypto.BIO_free
+-
+-# openssl/x509.h
+-X509_REQ_new = _libcrypto.X509_REQ_new
+-X509_REQ_free = _libcrypto.X509_REQ_free
+-X509_REQ_set_pubkey = _libcrypto.X509_REQ_set_pubkey
+-d2i_PUBKEY_bio = _libcrypto.d2i_PUBKEY_bio
+-i2d_X509_REQ_INFO = _libcrypto.i2d_X509_REQ_INFO
+-X509_NAME_add_entry_by_OBJ = _libcrypto.X509_NAME_add_entry_by_OBJ
+-X509_NAME_entry_count = _libcrypto.X509_NAME_entry_count
+-
+-
+-def X509_REQ_get_subject_name(req):
+-    return req.req_info.subject
+-
+-
+-# openssl/objects.h
+-OBJ_txt2obj = _libcrypto.OBJ_txt2obj
+-
+-# openssl/evp.h
+-EVP_PKEY_free = _libcrypto.EVP_PKEY_free
+-
+-# openssl/asn1.h
+-MBSTRING_UTF8 = 0x1000
+-
+-# openssl/x509v3.h
+-X509V3_set_ctx = _libcrypto.X509V3_set_ctx
+-X509V3_set_nconf = _libcrypto.X509V3_set_nconf
+-X509V3_EXT_REQ_add_nconf = _libcrypto.X509V3_EXT_REQ_add_nconf
+-
+-# openssl/err.h
+-ERR_get_error = _libcrypto.ERR_get_error
+-ERR_error_string = _libcrypto.ERR_error_string
+-
+-
+-def _raise_openssl_errors():
+-    msgs = []
+-
+-    code = ERR_get_error()
+-    while code != 0:
+-        msg = _ffi.string(ERR_error_string(code, NULL))
+-        try:
+-            strmsg = msg.decode('utf-8')
+-        except UnicodeDecodeError:
+-            strmsg = repr(msg)
+-        msgs.append(strmsg)
+-        code = ERR_get_error()
+-
+-    raise errors.CSRTemplateError(reason='\n'.join(msgs))
+-
+-
+-def _parse_dn_section(subj, dn_sk):
+-    for i in range(sk_CONF_VALUE_num(dn_sk)):
+-        v = sk_CONF_VALUE_value(dn_sk, i)
+-        rdn_type = _ffi.string(v.name)
+-
+-        # Skip past any leading X. X: X, etc to allow for multiple instances
+-        for idx, c in enumerate(rdn_type):
+-            if c in b':,.':
+-                if idx+1 < len(rdn_type):
+-                    rdn_type = rdn_type[idx+1:]
+-                break
+-        if rdn_type.startswith(b'+'):
+-            rdn_type = rdn_type[1:]
+-            mval = -1
+-        else:
+-            mval = 0
+-
+-        # convert rdn_type to an OID
+-        #
+-        # OpenSSL is fussy about the case of the string.  For example,
+-        # lower-case 'o' (for "organization name") is not recognised.
+-        # Therefore, try to convert the given string into an OID.  If
+-        # that fails, convert it upper case and try again.
+-        #
+-        oid = OBJ_txt2obj(rdn_type, 0)
+-        if oid == NULL:
+-            oid = OBJ_txt2obj(rdn_type.upper(), 0)
+-        if oid == NULL:
+-            raise errors.CSRTemplateError(
+-                reason='unrecognised attribute type: {}'
+-                .format(rdn_type.decode('utf-8')))
+-
+-        if not X509_NAME_add_entry_by_OBJ(
+-                subj, oid, MBSTRING_UTF8,
+-                _ffi.cast("unsigned char *", v.value), -1, -1, mval):
+-            _raise_openssl_errors()
+-
+-    if not X509_NAME_entry_count(subj):
+-        raise errors.CSRTemplateError(
+-            reason='error, subject in config file is empty')
+-
+-
+-def build_requestinfo(config, public_key_info):
+-    '''
+-    Return a cffi buffer containing a DER-encoded CertificationRequestInfo.
+-
+-    The returned object implements the buffer protocol.
+-
+-    '''
+-    reqdata = NULL
+-    req = NULL
+-    nconf_bio = NULL
+-    pubkey_bio = NULL
+-    pubkey = NULL
+-
+-    try:
+-        reqdata = NCONF_new(NULL)
+-        if reqdata == NULL:
+-            _raise_openssl_errors()
+-
+-        nconf_bio = BIO_new_mem_buf(config, len(config))
+-        errorline = _ffi.new('long[1]', [-1])
+-        i = NCONF_load_bio(reqdata, nconf_bio, errorline)
+-        if i < 0:
+-            if errorline[0] < 0:
+-                raise errors.CSRTemplateError(reason="Can't load config file")
+-            else:
+-                raise errors.CSRTemplateError(
+-                    reason='Error on line %d of config file' % errorline[0])
+-
+-        dn_sect = NCONF_get_string(reqdata, b'req', b'distinguished_name')
+-        if dn_sect == NULL:
+-            raise errors.CSRTemplateError(
+-                reason='Unable to find "distinguished_name" key in config')
+-
+-        dn_sk = NCONF_get_section(reqdata, dn_sect)
+-        if dn_sk == NULL:
+-            raise errors.CSRTemplateError(
+-                reason='Unable to find "%s" section in config' %
+-                _ffi.string(dn_sect))
+-
+-        pubkey_bio = BIO_new_mem_buf(public_key_info, len(public_key_info))
+-        pubkey = d2i_PUBKEY_bio(pubkey_bio, NULL)
+-        if pubkey == NULL:
+-            _raise_openssl_errors()
+-
+-        req = X509_REQ_new()
+-        if req == NULL:
+-            _raise_openssl_errors()
+-
+-        subject = X509_REQ_get_subject_name(req)
+-
+-        _parse_dn_section(subject, dn_sk)
+-
+-        if not X509_REQ_set_pubkey(req, pubkey):
+-            _raise_openssl_errors()
+-
+-        ext_ctx = _ffi.new("X509V3_CTX[1]")
+-        X509V3_set_ctx(ext_ctx, NULL, NULL, req, NULL, 0)
+-        X509V3_set_nconf(ext_ctx, reqdata)
+-
+-        extn_section = NCONF_get_string(reqdata, b"req", b"req_extensions")
+-        if extn_section != NULL:
+-            if not X509V3_EXT_REQ_add_nconf(
+-                    reqdata, ext_ctx, extn_section, req):
+-                _raise_openssl_errors()
+-
+-        if _openssl_version < 0x10100000 or LIBRESSL:
+-            der_len = i2d_X509_REQ_INFO(req.req_info, NULL)
+-        else:
+-            req_info = _ffi.new("X509_REQ_INFO *", req.req_info)
+-            der_len = i2d_X509_REQ_INFO(req_info, NULL)
+-            req.req_info = req_info[0]
+-        if der_len < 0:
+-            _raise_openssl_errors()
+-
+-        der_buf = _ffi.new("unsigned char[%d]" % der_len)
+-        der_out = _ffi.new("unsigned char **", der_buf)
+-        if _openssl_version < 0x10100000 or LIBRESSL:
+-            der_len = i2d_X509_REQ_INFO(req.req_info, der_out)
+-        else:
+-            der_len = i2d_X509_REQ_INFO(req_info, der_out)
+-            req.req_info = req_info[0]
+-        if der_len < 0:
+-            _raise_openssl_errors()
+-
+-        return _ffi.buffer(der_buf, der_len)
+-
+-    finally:
+-        if reqdata != NULL:
+-            NCONF_free(reqdata)
+-        if req != NULL:
+-            X509_REQ_free(req)
+-        if nconf_bio != NULL:
+-            BIO_free(nconf_bio)
+-        if pubkey_bio != NULL:
+-            BIO_free(pubkey_bio)
+-        if pubkey != NULL:
+-            EVP_PKEY_free(pubkey)
+diff -urN freeipa-4.8.0/ipaclient/csrgen.py freeipa-4.8.0.removed_csrgen/ipaclient/csrgen.py
+--- freeipa-4.8.0/ipaclient/csrgen.py	2019-07-03 08:42:41.811540288 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/csrgen.py	1970-01-01 01:00:00.000000000 +0100
+@@ -1,488 +0,0 @@
+-#
+-# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+-#
+-
+-import base64
+-import collections
+-import errno
+-import json
+-import logging
+-import os
+-import os.path
+-import pipes
+-import subprocess
+-import traceback
+-import codecs
+-
+-import pkg_resources
+-
+-from cryptography.hazmat.backends import default_backend
+-from cryptography.hazmat.primitives.asymmetric import padding
+-from cryptography.hazmat.primitives import hashes
+-from cryptography.hazmat.primitives.serialization import (
+-    load_pem_private_key, Encoding, PublicFormat)
+-from cryptography.x509 import load_pem_x509_certificate
+-import jinja2
+-import jinja2.ext
+-import jinja2.sandbox
+-from pyasn1.codec.der import decoder, encoder
+-from pyasn1.type import univ
+-from pyasn1_modules import rfc2314
+-import six
+-
+-from ipalib import api
+-from ipalib import errors
+-from ipalib.text import _
+-
+-if six.PY3:
+-    unicode = str
+-
+-__doc__ = _("""
+-Routines for constructing certificate signing requests using IPA data and
+-stored templates.
+-""")
+-
+-logger = logging.getLogger(__name__)
+-
+-
+-class IndexableUndefined(jinja2.Undefined):
+-    def __getitem__(self, key):
+-        return jinja2.Undefined(
+-            hint=self._undefined_hint, obj=self._undefined_obj,
+-            name=self._undefined_name, exc=self._undefined_exception)
+-
+-
+-class IPAExtension(jinja2.ext.Extension):
+-    """Jinja2 extension providing useful features for CSR generation rules."""
+-
+-    def __init__(self, environment):
+-        super(IPAExtension, self).__init__(environment)
+-
+-        environment.filters.update(
+-            quote=self.quote,
+-            required=self.required,
+-        )
+-
+-    def quote(self, data):
+-        return pipes.quote(data)
+-
+-    def required(self, data, name):
+-        if not data:
+-            raise errors.CSRTemplateError(
+-                reason=_(
+-                    'Required CSR generation rule %(name)s is missing data') %
+-                {'name': name})
+-        return data
+-
+-
+-class Formatter:
+-    """
+-    Class for processing a set of CSR generation rules into a template.
+-
+-    The template can be rendered with user and database data to produce a
+-    config, which specifies how to build a CSR.
+-
+-    Subclasses of Formatter should set the value of base_template_name to the
+-    filename of a base template with spaces for the processed rules.
+-    Additionally, they should override the _get_template_params method to
+-    produce the correct output for the base template.
+-    """
+-    base_template_name = None
+-
+-    def __init__(self, csr_data_dir=None):
+-        # chain loaders:
+-        # 1) csr_data_dir/templates
+-        # 2) /etc/ipa/csrgen/templates
+-        # 3) ipaclient/csrgen/templates
+-        loaders = []
+-        if csr_data_dir is not None:
+-            loaders.append(jinja2.FileSystemLoader(
+-                os.path.join(csr_data_dir, 'templates'))
+-            )
+-        loaders.append(jinja2.FileSystemLoader(
+-            os.path.join(api.env.confdir, 'csrgen/templates'))
+-        )
+-        loaders.append(jinja2.PackageLoader('ipaclient', 'csrgen/templates'))
+-
+-        self.jinja2 = jinja2.sandbox.SandboxedEnvironment(
+-            loader=jinja2.ChoiceLoader(loaders),
+-            extensions=[jinja2.ext.ExprStmtExtension, IPAExtension],
+-            keep_trailing_newline=True, undefined=IndexableUndefined)
+-
+-        self.passthrough_globals = {}
+-
+-    def _define_passthrough(self, call):
+-        """Some macros are meant to be interpreted during the final render, not
+-        when data rules are interpolated into syntax rules. This method allows
+-        those macros to be registered so that calls to them are passed through
+-        to the prepared rule rather than interpreted.
+-        """
+-
+-        def passthrough(caller):
+-            return u'{%% call %s() %%}%s{%% endcall %%}' % (call, caller())
+-
+-        parts = call.split('.')
+-        current_level = self.passthrough_globals
+-        for part in parts[:-1]:
+-            if part not in current_level:
+-                current_level[part] = {}
+-            current_level = current_level[part]
+-        current_level[parts[-1]] = passthrough
+-
+-    def build_template(self, rules):
+-        """
+-        Construct a template that can produce CSR generator strings.
+-
+-        :param rules: list of FieldMapping to use to populate the template.
+-
+-        :returns: jinja2.Template that can be rendered to produce the CSR data.
+-        """
+-        syntax_rules = []
+-        for field_mapping in rules:
+-            data_rules_prepared = [
+-                self._prepare_data_rule(rule)
+-                for rule in field_mapping.data_rules]
+-
+-            data_sources = []
+-            for xrule in field_mapping.data_rules:
+-                data_source = xrule.options.get('data_source')
+-                if data_source:
+-                    data_sources.append(data_source)
+-
+-            syntax_rules.append(self._prepare_syntax_rule(
+-                field_mapping.syntax_rule, data_rules_prepared,
+-                field_mapping.description, data_sources))
+-
+-        template_params = self._get_template_params(syntax_rules)
+-        base_template = self.jinja2.get_template(
+-            self.base_template_name, globals=self.passthrough_globals)
+-
+-        try:
+-            combined_template_source = base_template.render(**template_params)
+-        except jinja2.UndefinedError:
+-            logger.debug(traceback.format_exc())
+-            raise errors.CSRTemplateError(reason=_(
+-                'Template error when formatting certificate data'))
+-
+-        logger.debug(
+-            'Formatting with template: %s', combined_template_source)
+-        combined_template = self.jinja2.from_string(combined_template_source)
+-
+-        return combined_template
+-
+-    def _wrap_conditional(self, rule, condition):
+-        rule = '{%% if %s %%}%s{%% endif %%}' % (condition, rule)
+-        return rule
+-
+-    def _wrap_required(self, rule, description):
+-        template = '{%% filter required("%s") %%}%s{%% endfilter %%}' % (
+-            description, rule)
+-
+-        return template
+-
+-    def _prepare_data_rule(self, data_rule):
+-        template = data_rule.template
+-
+-        data_source = data_rule.options.get('data_source')
+-        if data_source:
+-            template = self._wrap_conditional(template, data_source)
+-
+-        return template
+-
+-    def _prepare_syntax_rule(
+-            self, syntax_rule, data_rules, description, data_sources):
+-        logger.debug('Syntax rule template: %s', syntax_rule.template)
+-        template = self.jinja2.from_string(
+-            syntax_rule.template, globals=self.passthrough_globals)
+-        is_required = syntax_rule.options.get('required', False)
+-        try:
+-            prepared_template = template.render(datarules=data_rules)
+-        except jinja2.UndefinedError:
+-            logger.debug(traceback.format_exc())
+-            raise errors.CSRTemplateError(reason=_(
+-                'Template error when formatting certificate data'))
+-
+-        if data_sources:
+-            combinator = ' %s ' % syntax_rule.options.get(
+-                'data_source_combinator', 'or')
+-            condition = combinator.join(data_sources)
+-            prepared_template = self._wrap_conditional(
+-                prepared_template, condition)
+-
+-        if is_required:
+-            prepared_template = self._wrap_required(
+-                prepared_template, description)
+-
+-        return prepared_template
+-
+-    def _get_template_params(self, syntax_rules):
+-        """
+-        Package the syntax rules into fields expected by the base template.
+-
+-        :param syntax_rules: list of prepared syntax rules to be included in
+-            the template.
+-
+-        :returns: dict of values needed to render the base template.
+-        """
+-        raise NotImplementedError('Formatter class must be subclassed')
+-
+-
+-class OpenSSLFormatter(Formatter):
+-    """Formatter class generating the openssl config-file format."""
+-
+-    base_template_name = 'openssl_base.tmpl'
+-
+-    # Syntax rules are wrapped in this data structure, to keep track of whether
+-    # each goes in the extension or the root section
+-    SyntaxRule = collections.namedtuple(
+-        'SyntaxRule', ['template', 'is_extension'])
+-
+-    def __init__(self, *args, **kwargs):
+-        super(OpenSSLFormatter, self).__init__(*args, **kwargs)
+-        self._define_passthrough('openssl.section')
+-
+-    def _get_template_params(self, syntax_rules):
+-        parameters = [rule.template for rule in syntax_rules
+-                      if not rule.is_extension]
+-        extensions = [rule.template for rule in syntax_rules
+-                      if rule.is_extension]
+-
+-        return {'parameters': parameters, 'extensions': extensions}
+-
+-    def _prepare_syntax_rule(
+-            self, syntax_rule, data_rules, description, data_sources):
+-        """Overrides method to pull out whether rule is an extension or not."""
+-        prepared_template = super(OpenSSLFormatter, self)._prepare_syntax_rule(
+-            syntax_rule, data_rules, description, data_sources)
+-        is_extension = syntax_rule.options.get('extension', False)
+-        return self.SyntaxRule(prepared_template, is_extension)
+-
+-
+-class FieldMapping:
+-    """Representation of the rules needed to construct a complete cert field.
+-
+-    Attributes:
+-        description: str, a name or description of this field, to be used in
+-            messages
+-        syntax_rule: Rule, the rule defining the syntax of this field
+-        data_rules: list of Rule, the rules that produce data to be stored in
+-            this field
+-    """
+-    __slots__ = ['description', 'syntax_rule', 'data_rules']
+-
+-    def __init__(self, description, syntax_rule, data_rules):
+-        self.description = description
+-        self.syntax_rule = syntax_rule
+-        self.data_rules = data_rules
+-
+-
+-class Rule:
+-    __slots__ = ['name', 'template', 'options']
+-
+-    def __init__(self, name, template, options):
+-        self.name = name
+-        self.template = template
+-        self.options = options
+-
+-
+-class RuleProvider:
+-    def rules_for_profile(self, profile_id):
+-        """
+-        Return the rules needed to build a CSR using the given profile.
+-
+-        :param profile_id: str, name of the CSR generation profile to use
+-
+-        :returns: list of FieldMapping, filled out with the appropriate rules
+-        """
+-        raise NotImplementedError('RuleProvider class must be subclassed')
+-
+-
+-class FileRuleProvider(RuleProvider):
+-    def __init__(self, csr_data_dir=None):
+-        self.rules = {}
+-        self._csrgen_data_dirs = []
+-        if csr_data_dir is not None:
+-            self._csrgen_data_dirs.append(csr_data_dir)
+-        self._csrgen_data_dirs.append(
+-            os.path.join(api.env.confdir, 'csrgen')
+-        )
+-        self._csrgen_data_dirs.append(
+-            pkg_resources.resource_filename('ipaclient', 'csrgen')
+-        )
+-
+-    def _open(self, subdir, filename):
+-        for data_dir in self._csrgen_data_dirs:
+-            path = os.path.join(data_dir, subdir, filename)
+-            try:
+-                return open(path)
+-            except IOError as e:
+-                if e.errno != errno.ENOENT:
+-                    raise
+-        raise IOError(
+-            errno.ENOENT,
+-            "'{}' not found in {}".format(
+-                os.path.join(subdir, filename),
+-                ", ".join(self._csrgen_data_dirs)
+-            )
+-        )
+-
+-    def _rule(self, rule_name):
+-        if rule_name not in self.rules:
+-            try:
+-                with self._open('rules', '%s.json' % rule_name) as f:
+-                    ruleconf = json.load(f)
+-            except IOError:
+-                raise errors.NotFound(
+-                    reason=_('No generation rule %(rulename)s found.') %
+-                    {'rulename': rule_name})
+-
+-            try:
+-                rule = ruleconf['rule']
+-            except KeyError:
+-                raise errors.EmptyResult(
+-                    reason=_('Generation rule "%(rulename)s" is missing the'
+-                             ' "rule" key') % {'rulename': rule_name})
+-
+-            options = ruleconf.get('options', {})
+-
+-            self.rules[rule_name] = Rule(
+-                rule_name, rule['template'], options)
+-
+-        return self.rules[rule_name]
+-
+-    def rules_for_profile(self, profile_id):
+-        try:
+-            with self._open('profiles', '%s.json' % profile_id) as f:
+-                profile = json.load(f)
+-        except IOError:
+-            raise errors.NotFound(
+-                reason=_('No CSR generation rules are defined for profile'
+-                         ' %(profile_id)s') % {'profile_id': profile_id})
+-
+-        field_mappings = []
+-        for field in profile:
+-            syntax_rule = self._rule(field['syntax'])
+-            data_rules = [self._rule(name) for name in field['data']]
+-            field_mappings.append(FieldMapping(
+-                syntax_rule.name, syntax_rule, data_rules))
+-        return field_mappings
+-
+-
+-class CSRGenerator:
+-    def __init__(self, rule_provider, formatter_class=OpenSSLFormatter):
+-        self.rule_provider = rule_provider
+-        self.formatter = formatter_class()
+-
+-    def csr_config(self, principal, config, profile_id):
+-        render_data = {'subject': principal, 'config': config}
+-
+-        rules = self.rule_provider.rules_for_profile(profile_id)
+-        template = self.formatter.build_template(rules)
+-
+-        try:
+-            config = template.render(render_data)
+-        except jinja2.UndefinedError:
+-            logger.debug(traceback.format_exc())
+-            raise errors.CSRTemplateError(reason=_(
+-                'Template error when formatting certificate data'))
+-
+-        return config
+-
+-
+-class CSRLibraryAdaptor:
+-    def get_subject_public_key_info(self):
+-        raise NotImplementedError('Use a subclass of CSRLibraryAdaptor')
+-
+-    def sign_csr(self, certification_request_info):
+-        """Sign a CertificationRequestInfo.
+-
+-        :returns: bytes, a DER-encoded signed CSR.
+-        """
+-        raise NotImplementedError('Use a subclass of CSRLibraryAdaptor')
+-
+-
+-class OpenSSLAdaptor:
+-    def __init__(self, key=None, key_filename=None, password_filename=None):
+-        """
+-        Must provide either ``key_filename`` or ``key``.
+-
+-        """
+-        if key_filename is not None:
+-            with open(key_filename, 'rb') as key_file:
+-                key_bytes = key_file.read()
+-
+-            password = None
+-            if password_filename is not None:
+-                with open(password_filename, 'rb') as password_file:
+-                    password = password_file.read().strip()
+-
+-            self._key = load_pem_private_key(
+-                key_bytes, password, default_backend())
+-
+-        elif key is not None:
+-            self._key = key
+-
+-        else:
+-            raise ValueError("Must provide 'key' or 'key_filename'")
+-
+-    def key(self):
+-        return self._key
+-
+-    def get_subject_public_key_info(self):
+-        pubkey_info = self.key().public_key().public_bytes(
+-            Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
+-        return pubkey_info
+-
+-    def sign_csr(self, certification_request_info):
+-        reqinfo = decoder.decode(
+-            certification_request_info, rfc2314.CertificationRequestInfo())[0]
+-        csr = rfc2314.CertificationRequest()
+-        csr.setComponentByName('certificationRequestInfo', reqinfo)
+-
+-        algorithm = rfc2314.SignatureAlgorithmIdentifier()
+-        algorithm.setComponentByName(
+-            'algorithm', univ.ObjectIdentifier(
+-                '1.2.840.113549.1.1.11'))  # sha256WithRSAEncryption
+-        csr.setComponentByName('signatureAlgorithm', algorithm)
+-
+-        signature = self.key().sign(
+-            certification_request_info,
+-            padding.PKCS1v15(),
+-            hashes.SHA256()
+-        )
+-        asn1sig = univ.BitString("'{sig}'H".format(
+-                                    sig=codecs.encode(signature, 'hex')
+-                                    .decode('ascii'))
+-                                 )
+-        csr.setComponentByName('signature', asn1sig)
+-        return encoder.encode(csr)
+-
+-
+-class NSSAdaptor:
+-    def __init__(self, database, password_filename):
+-        self.database = database
+-        self.password_filename = password_filename
+-        self.nickname = base64.b32encode(os.urandom(40))
+-
+-    def get_subject_public_key_info(self):
+-        temp_cn = base64.b32encode(os.urandom(40)).decode('ascii')
+-
+-        password_args = []
+-        if self.password_filename is not None:
+-            password_args = ['-f', self.password_filename]
+-
+-        subprocess.check_call(
+-            ['certutil', '-S', '-n', self.nickname, '-s', 'CN=%s' % temp_cn,
+-             '-x', '-t', ',,', '-d', self.database] + password_args)
+-        cert_pem = subprocess.check_output(
+-            ['certutil', '-L', '-n', self.nickname, '-a',
+-             '-d', self.database] + password_args)
+-
+-        cert = load_pem_x509_certificate(cert_pem, default_backend())
+-        pubkey_info = cert.public_key().public_bytes(
+-            Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
+-
+-        return pubkey_info
+-
+-    def sign_csr(self, certification_request_info):
+-        raise NotImplementedError('NSS is not yet supported')
+diff -urN freeipa-4.8.0/ipaclient/plugins/cert.py freeipa-4.8.0.removed_csrgen/ipaclient/plugins/cert.py
+--- freeipa-4.8.0/ipaclient/plugins/cert.py	2019-07-03 08:42:41.978537802 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/plugins/cert.py	2019-07-03 13:24:38.477222594 +0200
+@@ -21,8 +21,6 @@
+ 
+ import base64
+ 
+-import six
+-
+ from ipaclient.frontend import MethodOverride
+ from ipalib import errors
+ from ipalib import x509
+@@ -31,9 +29,6 @@
+ from ipalib.plugable import Registry
+ from ipalib.text import _
+ 
+-if six.PY3:
+-    unicode = str
+-
+ register = Registry()
+ 
+ 
+@@ -73,87 +68,12 @@
+ 
+ @register(override=True, no_fail=True)
+ class cert_request(CertRetrieveOverride):
+-    takes_options = CertRetrieveOverride.takes_options + (
+-        Str(
+-            'database?',
+-            label=_('Path to NSS database'),
+-            doc=_('Path to NSS database to use for private key'),
+-        ),
+-        Str(
+-            'private_key?',
+-            label=_('Path to private key file'),
+-            doc=_('Path to PEM file containing a private key'),
+-        ),
+-        Str(
+-            'password_file?',
+-            label=_(
+-                'File containing a password for the private key or database'),
+-        ),
+-        Str(
+-            'csr_profile_id?',
+-            label=_('Name of CSR generation profile (if not the same as'
+-                    ' profile_id)'),
+-        ),
+-    )
+-
+     def get_args(self):
+         for arg in super(cert_request, self).get_args():
+             if arg.name == 'csr':
+                 arg = arg.clone_retype(arg.name, File, required=False)
+             yield arg
+ 
+-    def forward(self, csr=None, **options):
+-        database = options.pop('database', None)
+-        private_key = options.pop('private_key', None)
+-        csr_profile_id = options.pop('csr_profile_id', None)
+-        password_file = options.pop('password_file', None)
+-
+-        if csr is None:
+-            # Deferred import, ipaclient.csrgen is expensive to load.
+-            # see https://pagure.io/freeipa/issue/7484
+-            from ipaclient import csrgen
+-
+-            if database:
+-                adaptor = csrgen.NSSAdaptor(database, password_file)
+-            elif private_key:
+-                adaptor = csrgen.OpenSSLAdaptor(
+-                    key_filename=private_key, password_filename=password_file)
+-            else:
+-                raise errors.InvocationError(
+-                    message=u"One of 'database' or 'private_key' is required")
+-
+-            pubkey_info = adaptor.get_subject_public_key_info()
+-            pubkey_info_b64 = base64.b64encode(pubkey_info)
+-
+-            # If csr_profile_id is passed, that takes precedence.
+-            # Otherwise, use profile_id. If neither are passed, the default
+-            # in cert_get_requestdata will be used.
+-            profile_id = csr_profile_id
+-            if profile_id is None:
+-                profile_id = options.get('profile_id')
+-
+-            response = self.api.Command.cert_get_requestdata(
+-                profile_id=profile_id,
+-                principal=options.get('principal'),
+-                public_key_info=pubkey_info_b64)
+-
+-            req_info_b64 = response['result']['request_info']
+-            req_info = base64.b64decode(req_info_b64)
+-
+-            csr = adaptor.sign_csr(req_info)
+-
+-            if not csr:
+-                raise errors.CertificateOperationError(
+-                    error=(_('Generated CSR was empty')))
+-
+-        else:
+-            if database is not None or private_key is not None:
+-                raise errors.MutuallyExclusiveError(reason=_(
+-                    "Options 'database' and 'private_key' are not compatible"
+-                    " with 'csr'"))
+-
+-        return super(cert_request, self).forward(csr, **options)
+-
+ 
+ @register(override=True, no_fail=True)
+ class cert_show(CertRetrieveOverride):
+diff -urN freeipa-4.8.0/ipaclient/plugins/cert.py.orig freeipa-4.8.0.removed_csrgen/ipaclient/plugins/cert.py.orig
+--- freeipa-4.8.0/ipaclient/plugins/cert.py.orig	1970-01-01 01:00:00.000000000 +0100
++++ freeipa-4.8.0.removed_csrgen/ipaclient/plugins/cert.py.orig	2019-07-03 13:24:38.478222573 +0200
+@@ -0,0 +1,215 @@
++# Authors:
++#   Andrew Wnuk <awnuk@redhat.com>
++#   Jason Gerard DeRose <jderose@redhat.com>
++#   John Dennis <jdennis@redhat.com>
++#
++# Copyright (C) 2009  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/>.
++
++import base64
++
++import six
++
++from ipaclient.frontend import MethodOverride
++from ipalib import errors
++from ipalib import x509
++from ipalib import util
++from ipalib.parameters import BinaryFile, File, Flag, Str
++from ipalib.plugable import Registry
++from ipalib.text import _
++
++if six.PY3:
++    unicode = str
++
++register = Registry()
++
++
++class CertRetrieveOverride(MethodOverride):
++    takes_options = (
++        Str(
++            'certificate_out?',
++            doc=_('Write certificate (chain if --chain used) to file'),
++            include='cli',
++            cli_metavar='FILE',
++        ),
++    )
++
++    def forward(self, *args, **options):
++        if 'certificate_out' in options:
++            certificate_out = options.pop('certificate_out')
++            try:
++                util.check_writable_file(certificate_out)
++            except errors.FileError as e:
++                raise errors.ValidationError(name='certificate-out',
++                                             error=str(e))
++        else:
++            certificate_out = None
++
++        result = super(CertRetrieveOverride, self).forward(*args, **options)
++
++        if certificate_out is not None:
++            if options.get('chain', False):
++                certs = result['result']['certificate_chain']
++            else:
++                certs = [base64.b64decode(result['result']['certificate'])]
++            certs = (x509.load_der_x509_certificate(cert) for cert in certs)
++            x509.write_certificate_list(certs, certificate_out)
++
++        return result
++
++
++@register(override=True, no_fail=True)
++class cert_request(CertRetrieveOverride):
++    takes_options = CertRetrieveOverride.takes_options + (
++        Str(
++            'database?',
++            label=_('Path to NSS database'),
++            doc=_('Path to NSS database to use for private key'),
++        ),
++        Str(
++            'private_key?',
++            label=_('Path to private key file'),
++            doc=_('Path to PEM file containing a private key'),
++        ),
++        Str(
++            'password_file?',
++            label=_(
++                'File containing a password for the private key or database'),
++        ),
++        Str(
++            'csr_profile_id?',
++            label=_('Name of CSR generation profile (if not the same as'
++                    ' profile_id)'),
++        ),
++    )
++
++    def get_args(self):
++        for arg in super(cert_request, self).get_args():
++            if arg.name == 'csr':
++                arg = arg.clone_retype(arg.name, File, required=False)
++            yield arg
++
++    def forward(self, csr=None, **options):
++        database = options.pop('database', None)
++        private_key = options.pop('private_key', None)
++        csr_profile_id = options.pop('csr_profile_id', None)
++        password_file = options.pop('password_file', None)
++
++        if csr is None:
++            # Deferred import, ipaclient.csrgen is expensive to load.
++            # see https://pagure.io/freeipa/issue/7484
++            from ipaclient import csrgen
++
++            if database:
++                adaptor = csrgen.NSSAdaptor(database, password_file)
++            elif private_key:
++                adaptor = csrgen.OpenSSLAdaptor(
++                    key_filename=private_key, password_filename=password_file)
++            else:
++                raise errors.InvocationError(
++                    message=u"One of 'database' or 'private_key' is required")
++
++            pubkey_info = adaptor.get_subject_public_key_info()
++            pubkey_info_b64 = base64.b64encode(pubkey_info)
++
++            # If csr_profile_id is passed, that takes precedence.
++            # Otherwise, use profile_id. If neither are passed, the default
++            # in cert_get_requestdata will be used.
++            profile_id = csr_profile_id
++            if profile_id is None:
++                profile_id = options.get('profile_id')
++
++            response = self.api.Command.cert_get_requestdata(
++                profile_id=profile_id,
++                principal=options.get('principal'),
++                public_key_info=pubkey_info_b64)
++
++            req_info_b64 = response['result']['request_info']
++            req_info = base64.b64decode(req_info_b64)
++
++            csr = adaptor.sign_csr(req_info)
++
++            if not csr:
++                raise errors.CertificateOperationError(
++                    error=(_('Generated CSR was empty')))
++
++        else:
++            if database is not None or private_key is not None:
++                raise errors.MutuallyExclusiveError(reason=_(
++                    "Options 'database' and 'private_key' are not compatible"
++                    " with 'csr'"))
++
++        return super(cert_request, self).forward(csr, **options)
++
++
++@register(override=True, no_fail=True)
++class cert_show(CertRetrieveOverride):
++    def get_options(self):
++        for option in super(cert_show, self).get_options():
++            if option.name == 'out':
++                # skip server-defined --out
++                continue
++            if option.name == 'certificate_out':
++                # add --out as a deprecated alias of --certificate-out
++                option = option.clone_rename(
++                    'out',
++                    cli_name='certificate_out',
++                    deprecated_cli_aliases={'out'},
++                )
++            yield option
++
++    def forward(self, *args, **options):
++        try:
++            options['certificate_out'] = options.pop('out')
++        except KeyError:
++            pass
++
++        return super(cert_show, self).forward(*args, **options)
++
++
++@register(override=True, no_fail=True)
++class cert_remove_hold(MethodOverride):
++    has_output_params = (
++        Flag('unrevoked',
++            label=_('Unrevoked'),
++        ),
++        Str('error_string',
++            label=_('Error'),
++        ),
++    )
++
++
++@register(override=True, no_fail=True)
++class cert_find(MethodOverride):
++    takes_options = (
++        BinaryFile(
++            'file?',
++            label=_("Input filename"),
++            doc=_('File to load the certificate from.'),
++            include='cli',
++        ),
++    )
++
++    def forward(self, *args, **options):
++        if self.api.env.context == 'cli':
++            if 'certificate' in options and 'file' in options:
++                raise errors.MutuallyExclusiveError(
++                    reason=_("cannot specify both raw certificate and file"))
++            if 'certificate' not in options and 'file' in options:
++                options['certificate'] = x509.load_unknown_x509_certificate(
++                                            options.pop('file'))
++
++        return super(cert_find, self).forward(*args, **options)
+diff -urN freeipa-4.8.0/ipaclient/plugins/csrgen.py freeipa-4.8.0.removed_csrgen/ipaclient/plugins/csrgen.py
+--- freeipa-4.8.0/ipaclient/plugins/csrgen.py	2019-07-03 08:42:41.990537623 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/plugins/csrgen.py	1970-01-01 01:00:00.000000000 +0100
+@@ -1,128 +0,0 @@
+-#
+-# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+-#
+-
+-import base64
+-
+-import six
+-
+-from ipalib import api
+-from ipalib import errors
+-from ipalib import output
+-from ipalib import util
+-from ipalib.frontend import Local, Str
+-from ipalib.parameters import Bytes, Principal
+-from ipalib.plugable import Registry
+-from ipalib.text import _
+-from ipapython import dogtag
+-
+-
+-if six.PY3:
+-    unicode = str
+-
+-register = Registry()
+-
+-__doc__ = _("""
+-Commands to build certificate requests automatically
+-""")
+-
+-
+-@register()
+-class cert_get_requestdata(Local):
+-    __doc__ = _('Gather data for a certificate signing request.')
+-
+-    NO_CLI = True
+-
+-    takes_options = (
+-        Principal(
+-            'principal',
+-            label=_('Principal'),
+-            doc=_('Principal for this certificate (e.g.'
+-                  ' HTTP/test.example.com)'),
+-        ),
+-        Str(
+-            'profile_id?',
+-            label=_('Profile ID'),
+-            doc=_('CSR Generation Profile to use'),
+-        ),
+-        Bytes(
+-            'public_key_info',
+-            label=_('Subject Public Key Info'),
+-            doc=_('DER-encoded SubjectPublicKeyInfo structure'),
+-        ),
+-        Str(
+-            'out?',
+-            doc=_('Write CertificationRequestInfo to file'),
+-        ),
+-    )
+-
+-    has_output = (
+-        output.Output(
+-            'result',
+-            type=dict,
+-            doc=_('Dictionary mapping variable name to value'),
+-        ),
+-    )
+-
+-    has_output_params = (
+-        Str(
+-            'request_info',
+-            label=_('CertificationRequestInfo structure'),
+-        )
+-    )
+-
+-    def execute(self, *args, **options):
+-        # Deferred import, ipaclient.csrgen is expensive to load.
+-        # see https://pagure.io/freeipa/issue/7484
+-        from ipaclient import csrgen
+-        from ipaclient import csrgen_ffi
+-
+-        if 'out' in options:
+-            util.check_writable_file(options['out'])
+-
+-        principal = options.get('principal')
+-        profile_id = options.get('profile_id')
+-        if profile_id is None:
+-            profile_id = dogtag.DEFAULT_PROFILE
+-        public_key_info = options.get('public_key_info')
+-        public_key_info = base64.b64decode(public_key_info)
+-
+-        if self.api.env.in_server:
+-            backend = self.api.Backend.ldap2
+-        else:
+-            backend = self.api.Backend.rpcclient
+-        if not backend.isconnected():
+-            backend.connect()
+-
+-        try:
+-            if principal.is_host:
+-                principal_obj = api.Command.host_show(
+-                    principal.hostname, all=True)
+-            elif principal.is_service:
+-                principal_obj = api.Command.service_show(
+-                    unicode(principal), all=True)
+-            elif principal.is_user:
+-                principal_obj = api.Command.user_show(
+-                    principal.username, all=True)
+-        except errors.NotFound:
+-            raise errors.NotFound(
+-                reason=_("The principal for this request doesn't exist."))
+-        principal_obj = principal_obj['result']
+-        config = api.Command.config_show()['result']
+-
+-        generator = csrgen.CSRGenerator(csrgen.FileRuleProvider())
+-
+-        csr_config = generator.csr_config(principal_obj, config, profile_id)
+-        request_info = base64.b64encode(csrgen_ffi.build_requestinfo(
+-            csr_config.encode('utf8'), public_key_info))
+-
+-        result = {}
+-        if 'out' in options:
+-            with open(options['out'], 'wb') as f:
+-                f.write(request_info)
+-        else:
+-            result = dict(request_info=request_info)
+-
+-        return dict(
+-            result=result
+-        )
+diff -urN freeipa-4.8.0/ipaclient/setup.py freeipa-4.8.0.removed_csrgen/ipaclient/setup.py
+--- freeipa-4.8.0/ipaclient/setup.py	2019-07-03 08:42:41.836539916 +0200
++++ freeipa-4.8.0.removed_csrgen/ipaclient/setup.py	2019-07-03 13:24:38.479222551 +0200
+@@ -41,13 +41,6 @@
+             "ipaclient.remote_plugins.2_156",
+             "ipaclient.remote_plugins.2_164",
+         ],
+-        package_data={
+-            'ipaclient': [
+-                'csrgen/profiles/*.json',
+-                'csrgen/rules/*.json',
+-                'csrgen/templates/*.tmpl',
+-            ],
+-        },
+         install_requires=[
+             "cryptography",
+             "ipalib",
+@@ -63,7 +56,6 @@
+         extras_require={
+             "install": ["ipaplatform"],
+             "otptoken_yubikey": ["python-yubico", "pyusb"],
+-            "csrgen": ["cffi", "jinja2"],
+             "ldap": ["python-ldap"],  # ipapython.ipaldap
+         },
+         zip_safe=False,
+diff -urN freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
+--- freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf	2019-07-03 08:42:45.972478335 +0200
++++ freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf	1970-01-01 01:00:00.000000000 +0100
+@@ -1,16 +0,0 @@
+-[ req ]
+-prompt = no
+-encrypt_key = no
+-
+-distinguished_name = sec0
+-req_extensions = sec2
+-
+-[ sec0 ]
+-O=DOMAIN.EXAMPLE.COM
+-CN=machine.example.com
+-
+-[ sec1 ]
+-DNS = machine.example.com
+-
+-[ sec2 ]
+-subjectAltName = @sec1
+diff -urN freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
+--- freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf	2019-07-03 08:42:45.976478276 +0200
++++ freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf	1970-01-01 01:00:00.000000000 +0100
+@@ -1,16 +0,0 @@
+-[ req ]
+-prompt = no
+-encrypt_key = no
+-
+-distinguished_name = sec0
+-req_extensions = sec2
+-
+-[ sec0 ]
+-O=DOMAIN.EXAMPLE.COM
+-CN=testuser
+-
+-[ sec1 ]
+-email = testuser@example.com
+-
+-[ sec2 ]
+-subjectAltName = @sec1
+diff -urN freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json
+--- freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json	2019-07-03 08:42:45.980478216 +0200
++++ freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,8 +0,0 @@
+-[
+-    {
+-        "syntax": "basic",
+-        "data": [
+-            "options"
+-        ]
+-    }
+-]
+diff -urN freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json
+--- freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json	2019-07-03 08:42:45.984478157 +0200
++++ freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,5 +0,0 @@
+-{
+-  "rule": {
+-    "template": "openssl_rule"
+-  }
+-}
+diff -urN freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/rules/options.json freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/rules/options.json
+--- freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/rules/options.json	2019-07-03 08:42:45.988478097 +0200
++++ freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/rules/options.json	1970-01-01 01:00:00.000000000 +0100
+@@ -1,8 +0,0 @@
+-{
+-  "rule": {
+-    "template": "openssl_rule"
+-  },
+-  "options": {
+-    "rule_option": true
+-  }
+-}
+diff -urN freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl
+--- freeipa-4.8.0/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl	2019-07-03 08:42:45.993478023 +0200
++++ freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl	1970-01-01 01:00:00.000000000 +0100
+@@ -1 +0,0 @@
+-{{ options|join(";") }}
+diff -urN freeipa-4.8.0/ipatests/test_ipaclient/test_csrgen.py freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/test_csrgen.py
+--- freeipa-4.8.0/ipatests/test_ipaclient/test_csrgen.py	2019-07-03 08:42:45.963478469 +0200
++++ freeipa-4.8.0.removed_csrgen/ipatests/test_ipaclient/test_csrgen.py	1970-01-01 01:00:00.000000000 +0100
+@@ -1,304 +0,0 @@
+-#
+-# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+-#
+-
+-import os
+-import pytest
+-
+-from cryptography.hazmat.backends import default_backend
+-from cryptography.hazmat.primitives.asymmetric import rsa
+-from cryptography import x509
+-
+-from ipaclient import csrgen, csrgen_ffi
+-from ipalib import errors
+-
+-BASE_DIR = os.path.dirname(__file__)
+-CSR_DATA_DIR = os.path.join(BASE_DIR, 'data', 'test_csrgen')
+-
+-
+-@pytest.fixture
+-def formatter():
+-    return csrgen.Formatter(csr_data_dir=CSR_DATA_DIR)
+-
+-
+-@pytest.fixture
+-def rule_provider():
+-    return csrgen.FileRuleProvider(csr_data_dir=CSR_DATA_DIR)
+-
+-
+-@pytest.fixture
+-def generator():
+-    return csrgen.CSRGenerator(csrgen.FileRuleProvider())
+-
+-
+-class StubRuleProvider(csrgen.RuleProvider):
+-    def __init__(self):
+-        self.syntax_rule = csrgen.Rule(
+-            'syntax', '{{datarules|join(",")}}', {})
+-        self.data_rule = csrgen.Rule('data', 'data_template', {})
+-        self.field_mapping = csrgen.FieldMapping(
+-            'example', self.syntax_rule, [self.data_rule])
+-        self.rules = [self.field_mapping]
+-
+-    def rules_for_profile(self, profile_id):
+-        return self.rules
+-
+-
+-class IdentityFormatter(csrgen.Formatter):
+-    base_template_name = 'identity_base.tmpl'
+-
+-    def __init__(self):
+-        super(IdentityFormatter, self).__init__(csr_data_dir=CSR_DATA_DIR)
+-
+-    def _get_template_params(self, syntax_rules):
+-        return {'options': syntax_rules}
+-
+-
+-class test_Formatter:
+-    def test_prepare_data_rule_with_data_source(self, formatter):
+-        data_rule = csrgen.Rule('uid', '{{subject.uid.0}}',
+-                                {'data_source': 'subject.uid.0'})
+-        prepared = formatter._prepare_data_rule(data_rule)
+-        assert prepared == '{% if subject.uid.0 %}{{subject.uid.0}}{% endif %}'
+-
+-    def test_prepare_data_rule_no_data_source(self, formatter):
+-        """Not a normal case, but we should handle it anyway"""
+-        data_rule = csrgen.Rule('uid', 'static_text', {})
+-        prepared = formatter._prepare_data_rule(data_rule)
+-        assert prepared == 'static_text'
+-
+-    def test_prepare_syntax_rule_with_data_sources(self, formatter):
+-        syntax_rule = csrgen.Rule(
+-            'example', '{{datarules|join(",")}}', {})
+-        data_rules = ['{{subject.field1}}', '{{subject.field2}}']
+-        data_sources = ['subject.field1', 'subject.field2']
+-        prepared = formatter._prepare_syntax_rule(
+-            syntax_rule, data_rules, 'example', data_sources)
+-
+-        assert prepared == (
+-            '{% if subject.field1 or subject.field2 %}{{subject.field1}},'
+-            '{{subject.field2}}{% endif %}')
+-
+-    def test_prepare_syntax_rule_with_combinator(self, formatter):
+-        syntax_rule = csrgen.Rule('example', '{{datarules|join(",")}}',
+-                                  {'data_source_combinator': 'and'})
+-        data_rules = ['{{subject.field1}}', '{{subject.field2}}']
+-        data_sources = ['subject.field1', 'subject.field2']
+-        prepared = formatter._prepare_syntax_rule(
+-            syntax_rule, data_rules, 'example', data_sources)
+-
+-        assert prepared == (
+-            '{% if subject.field1 and subject.field2 %}{{subject.field1}},'
+-            '{{subject.field2}}{% endif %}')
+-
+-    def test_prepare_syntax_rule_required(self, formatter):
+-        syntax_rule = csrgen.Rule('example', '{{datarules|join(",")}}',
+-                                  {'required': True})
+-        data_rules = ['{{subject.field1}}']
+-        data_sources = ['subject.field1']
+-        prepared = formatter._prepare_syntax_rule(
+-            syntax_rule, data_rules, 'example', data_sources)
+-
+-        assert prepared == (
+-            '{% filter required("example") %}{% if subject.field1 %}'
+-            '{{subject.field1}}{% endif %}{% endfilter %}')
+-
+-    def test_prepare_syntax_rule_passthrough(self, formatter):
+-        """
+-        Calls to macros defined as passthrough are still call tags in the final
+-        template.
+-        """
+-        formatter._define_passthrough('example.macro')
+-
+-        syntax_rule = csrgen.Rule(
+-            'example',
+-            '{% call example.macro() %}{{datarules|join(",")}}{% endcall %}',
+-            {})
+-        data_rules = ['{{subject.field1}}']
+-        data_sources = ['subject.field1']
+-        prepared = formatter._prepare_syntax_rule(
+-            syntax_rule, data_rules, 'example', data_sources)
+-
+-        assert prepared == (
+-            '{% if subject.field1 %}{% call example.macro() %}'
+-            '{{subject.field1}}{% endcall %}{% endif %}')
+-
+-    def test_prepare_syntax_rule_no_data_sources(self, formatter):
+-        """Not a normal case, but we should handle it anyway"""
+-        syntax_rule = csrgen.Rule(
+-            'example', '{{datarules|join(",")}}', {})
+-        data_rules = ['rule1', 'rule2']
+-        data_sources = []
+-        prepared = formatter._prepare_syntax_rule(
+-            syntax_rule, data_rules, 'example', data_sources)
+-
+-        assert prepared == 'rule1,rule2'
+-
+-
+-class test_FileRuleProvider:
+-    def test_rule_basic(self, rule_provider):
+-        rule_name = 'basic'
+-
+-        rule = rule_provider._rule(rule_name)
+-
+-        assert rule.template == 'openssl_rule'
+-
+-    def test_rule_global_options(self, rule_provider):
+-        rule_name = 'options'
+-
+-        rule = rule_provider._rule(rule_name)
+-
+-        assert rule.options['rule_option'] is True
+-
+-    def test_rule_nosuchrule(self, rule_provider):
+-        with pytest.raises(errors.NotFound):
+-            rule_provider._rule('nosuchrule')
+-
+-    def test_rules_for_profile_success(self, rule_provider):
+-        rules = rule_provider.rules_for_profile('profile')
+-
+-        assert len(rules) == 1
+-        field_mapping = rules[0]
+-        assert field_mapping.syntax_rule.name == 'basic'
+-        assert len(field_mapping.data_rules) == 1
+-        assert field_mapping.data_rules[0].name == 'options'
+-
+-    def test_rules_for_profile_nosuchprofile(self, rule_provider):
+-        with pytest.raises(errors.NotFound):
+-            rule_provider.rules_for_profile('nosuchprofile')
+-
+-
+-class test_CSRGenerator:
+-    def test_userCert_OpenSSL(self, generator):
+-        principal = {
+-            'uid': ['testuser'],
+-            'mail': ['testuser@example.com'],
+-        }
+-        config = {
+-            'ipacertificatesubjectbase': [
+-                'O=DOMAIN.EXAMPLE.COM'
+-            ],
+-        }
+-
+-        script = generator.csr_config(principal, config, 'userCert')
+-        with open(os.path.join(
+-                CSR_DATA_DIR, 'configs', 'userCert.conf')) as f:
+-            expected_script = f.read()
+-        assert script == expected_script
+-
+-    def test_caIPAserviceCert_OpenSSL(self, generator):
+-        principal = {
+-            'krbprincipalname': [
+-                'HTTP/machine.example.com@DOMAIN.EXAMPLE.COM'
+-            ],
+-        }
+-        config = {
+-            'ipacertificatesubjectbase': [
+-                'O=DOMAIN.EXAMPLE.COM'
+-            ],
+-        }
+-
+-        script = generator.csr_config(
+-            principal, config, 'caIPAserviceCert')
+-        with open(os.path.join(
+-                CSR_DATA_DIR, 'configs', 'caIPAserviceCert.conf')) as f:
+-            expected_script = f.read()
+-        assert script == expected_script
+-
+-    def test_works_with_lowercase_attr_type_shortname(self, generator):
+-        principal = {
+-            'uid': ['testuser'],
+-            'mail': ['testuser@example.com'],
+-        }
+-        template_env = {
+-            'ipacertificatesubjectbase': [
+-                'o=DOMAIN.EXAMPLE.COM'  # lower-case attr type shortname
+-            ],
+-        }
+-        config = generator.csr_config(principal, template_env, 'userCert')
+-
+-        key = rsa.generate_private_key(
+-            public_exponent=65537,
+-            key_size=2048,
+-            backend=default_backend(),
+-        )
+-        adaptor = csrgen.OpenSSLAdaptor(key=key)
+-
+-        reqinfo = bytes(csrgen_ffi.build_requestinfo(
+-            config.encode('utf-8'), adaptor.get_subject_public_key_info()))
+-        csr_der = adaptor.sign_csr(reqinfo)
+-        csr = x509.load_der_x509_csr(csr_der, default_backend())
+-        assert (
+-            csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
+-            == [x509.NameAttribute(x509.NameOID.COMMON_NAME, u'testuser')]
+-        )
+-        assert (
+-            csr.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATION_NAME)
+-            == [x509.NameAttribute(
+-                x509.NameOID.ORGANIZATION_NAME, u'DOMAIN.EXAMPLE.COM')]
+-        )
+-
+-    def test_unrecognised_attr_type_raises(self, generator):
+-        principal = {
+-            'uid': ['testuser'],
+-            'mail': ['testuser@example.com'],
+-        }
+-        template_env = {
+-            'ipacertificatesubjectbase': [
+-                'X=DOMAIN.EXAMPLE.COM'  # unrecognised attr type
+-            ],
+-        }
+-        config = generator.csr_config(principal, template_env, 'userCert')
+-
+-        key = rsa.generate_private_key(
+-            public_exponent=65537,
+-            key_size=2048,
+-            backend=default_backend(),
+-        )
+-        adaptor = csrgen.OpenSSLAdaptor(key=key)
+-
+-        with pytest.raises(
+-                errors.CSRTemplateError,
+-                match=r'^unrecognised attribute type: X$'):
+-            csrgen_ffi.build_requestinfo(
+-                config.encode('utf-8'), adaptor.get_subject_public_key_info())
+-
+-
+-class test_rule_handling:
+-    def test_optionalAttributeMissing(self, generator):
+-        principal = {'uid': 'testuser'}
+-        rule_provider = StubRuleProvider()
+-        rule_provider.data_rule.template = '{{subject.mail}}'
+-        rule_provider.data_rule.options = {'data_source': 'subject.mail'}
+-        generator = csrgen.CSRGenerator(
+-            rule_provider, formatter_class=IdentityFormatter)
+-
+-        script = generator.csr_config(
+-            principal, {}, 'example')
+-        assert script == '\n'
+-
+-    def test_twoDataRulesOneMissing(self, generator):
+-        principal = {'uid': 'testuser'}
+-        rule_provider = StubRuleProvider()
+-        rule_provider.data_rule.template = '{{subject.mail}}'
+-        rule_provider.data_rule.options = {'data_source': 'subject.mail'}
+-        rule_provider.field_mapping.data_rules.append(csrgen.Rule(
+-            'data2', '{{subject.uid}}', {'data_source': 'subject.uid'}))
+-        generator = csrgen.CSRGenerator(
+-            rule_provider, formatter_class=IdentityFormatter)
+-
+-        script = generator.csr_config(principal, {}, 'example')
+-        assert script == ',testuser\n'
+-
+-    def test_requiredAttributeMissing(self):
+-        principal = {'uid': 'testuser'}
+-        rule_provider = StubRuleProvider()
+-        rule_provider.data_rule.template = '{{subject.mail}}'
+-        rule_provider.data_rule.options = {'data_source': 'subject.mail'}
+-        rule_provider.syntax_rule.options = {'required': True}
+-        generator = csrgen.CSRGenerator(
+-            rule_provider, formatter_class=IdentityFormatter)
+-
+-        with pytest.raises(errors.CSRTemplateError):
+-            _script = generator.csr_config(
+-                principal, {}, 'example')
diff --git a/SOURCES/1002-Remove-csrgen.patch b/SOURCES/1002-Remove-csrgen.patch
deleted file mode 100644
index 596f1ea..0000000
--- a/SOURCES/1002-Remove-csrgen.patch
+++ /dev/null
@@ -1,1804 +0,0 @@
-From 468bcf90cb985e2b1eb394bd752dc39aa4b75582 Mon Sep 17 00:00:00 2001
-From: Rob Crittenden <rcritten@redhat.com>
-Date: Thu, 19 Jul 2018 18:37:18 -0400
-Subject: [PATCH] Remove csrgen
-
-This reverts commits:
-* 72de679eb445c975ec70cd265d37d4927823ce5b
-* 177f07e163d6d591a1e609d35e0a6f6f5347551e
-* 80be18162921268be9c8981495c9e8a4de0c85cd
-* 83e2c2b65eeb5a3aa4a59c0535e9177aac5e4637
-* ada91c20588046bb147fc701718d3da4d2c080ca
-* 4350dcdea22fd2284836315d0ae7d38733a7620e
-* 39a5d9c5aae77687f67d9be02457733bdfb99ead
-* a26cf0d7910dd4c0a4da08682b4be8d3d94ba520
-* afd7c05d11432304bfdf183832a21d419f363689
-* f1a1c6eca1b294f24174d7b0e1f78de46d9d5b05
-* fc58eff6a3d7fe805e612b8b002304d8b9cd4ba9
-* 10ef5947860f5098182b1f95c08c1158e2da15f9
-
-https://bugzilla.redhat.com/show_bug.cgi?id=1432630
----
- freeipa.spec.in                                    |  14 -
- ipaclient/csrgen.py                                | 488 ---------------------
- ipaclient/csrgen/profiles/caIPAserviceCert.json    |  15 -
- ipaclient/csrgen/profiles/userCert.json            |  15 -
- ipaclient/csrgen/rules/dataDNS.json                |   8 -
- ipaclient/csrgen/rules/dataEmail.json              |   8 -
- ipaclient/csrgen/rules/dataHostCN.json             |   8 -
- ipaclient/csrgen/rules/dataSubjectBase.json        |   8 -
- ipaclient/csrgen/rules/dataUsernameCN.json         |   8 -
- ipaclient/csrgen/rules/syntaxSAN.json              |   8 -
- ipaclient/csrgen/rules/syntaxSubject.json          |   9 -
- ipaclient/csrgen/templates/openssl_base.tmpl       |  17 -
- ipaclient/csrgen/templates/openssl_macros.tmpl     |  29 --
- ipaclient/csrgen_ffi.py                            | 331 --------------
- ipaclient/plugins/cert.py                          |  80 ----
- ipaclient/plugins/csrgen.py                        | 128 ------
- ipaclient/setup.py                                 |   8 -
- .../data/test_csrgen/configs/caIPAserviceCert.conf |  16 -
- .../data/test_csrgen/configs/userCert.conf         |  16 -
- .../data/test_csrgen/profiles/profile.json         |   8 -
- .../data/test_csrgen/rules/basic.json              |   5 -
- .../data/test_csrgen/rules/options.json            |   8 -
- .../data/test_csrgen/templates/identity_base.tmpl  |   1 -
- ipatests/test_ipaclient/test_csrgen.py             | 304 -------------
- 24 files changed, 1540 deletions(-)
- delete mode 100644 ipaclient/csrgen.py
- delete mode 100644 ipaclient/csrgen/profiles/caIPAserviceCert.json
- delete mode 100644 ipaclient/csrgen/profiles/userCert.json
- delete mode 100644 ipaclient/csrgen/rules/dataDNS.json
- delete mode 100644 ipaclient/csrgen/rules/dataEmail.json
- delete mode 100644 ipaclient/csrgen/rules/dataHostCN.json
- delete mode 100644 ipaclient/csrgen/rules/dataSubjectBase.json
- delete mode 100644 ipaclient/csrgen/rules/dataUsernameCN.json
- delete mode 100644 ipaclient/csrgen/rules/syntaxSAN.json
- delete mode 100644 ipaclient/csrgen/rules/syntaxSubject.json
- delete mode 100644 ipaclient/csrgen/templates/openssl_base.tmpl
- delete mode 100644 ipaclient/csrgen/templates/openssl_macros.tmpl
- delete mode 100644 ipaclient/csrgen_ffi.py
- delete mode 100644 ipaclient/plugins/csrgen.py
- delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
- delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
- delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json
- delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/rules/basic.json
- delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/rules/options.json
- delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl
- delete mode 100644 ipatests/test_ipaclient/test_csrgen.py
-
-diff --git a/freeipa.spec.in b/freeipa.spec.in
-index a01fae7..cd1773a 100644
---- a/freeipa.spec.in
-+++ b/freeipa.spec.in
-@@ -1538,13 +1538,6 @@ fi
- %{python2_sitelib}/ipaclient/remote_plugins/*.py*
- %dir %{python2_sitelib}/ipaclient/remote_plugins/2_*
- %{python2_sitelib}/ipaclient/remote_plugins/2_*/*.py*
--%dir %{python2_sitelib}/ipaclient/csrgen
--%dir %{python2_sitelib}/ipaclient/csrgen/profiles
--%{python2_sitelib}/ipaclient/csrgen/profiles/*.json
--%dir %{python2_sitelib}/ipaclient/csrgen/rules
--%{python2_sitelib}/ipaclient/csrgen/rules/*.json
--%dir %{python2_sitelib}/ipaclient/csrgen/templates
--%{python2_sitelib}/ipaclient/csrgen/templates/*.tmpl
- %{python2_sitelib}/ipaclient-*.egg-info
- 
- %endif # with_python2
-@@ -1567,13 +1560,6 @@ fi
- %dir %{python3_sitelib}/ipaclient/remote_plugins/2_*
- %{python3_sitelib}/ipaclient/remote_plugins/2_*/*.py
- %{python3_sitelib}/ipaclient/remote_plugins/2_*/__pycache__/*.py*
--%dir %{python3_sitelib}/ipaclient/csrgen
--%dir %{python3_sitelib}/ipaclient/csrgen/profiles
--%{python3_sitelib}/ipaclient/csrgen/profiles/*.json
--%dir %{python3_sitelib}/ipaclient/csrgen/rules
--%{python3_sitelib}/ipaclient/csrgen/rules/*.json
--%dir %{python3_sitelib}/ipaclient/csrgen/templates
--%{python3_sitelib}/ipaclient/csrgen/templates/*.tmpl
- %{python3_sitelib}/ipaclient-*.egg-info
- 
- 
-diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py
-deleted file mode 100644
-index e7573be..0000000
---- a/ipaclient/csrgen.py
-+++ /dev/null
-@@ -1,488 +0,0 @@
--#
--# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
--#
--
--import base64
--import collections
--import errno
--import json
--import logging
--import os
--import os.path
--import pipes
--import subprocess
--import traceback
--import codecs
--
--import pkg_resources
--
--from cryptography.hazmat.backends import default_backend
--from cryptography.hazmat.primitives.asymmetric import padding
--from cryptography.hazmat.primitives import hashes
--from cryptography.hazmat.primitives.serialization import (
--    load_pem_private_key, Encoding, PublicFormat)
--from cryptography.x509 import load_pem_x509_certificate
--import jinja2
--import jinja2.ext
--import jinja2.sandbox
--from pyasn1.codec.der import decoder, encoder
--from pyasn1.type import univ
--from pyasn1_modules import rfc2314
--import six
--
--from ipalib import api
--from ipalib import errors
--from ipalib.text import _
--
--if six.PY3:
--    unicode = str
--
--__doc__ = _("""
--Routines for constructing certificate signing requests using IPA data and
--stored templates.
--""")
--
--logger = logging.getLogger(__name__)
--
--
--class IndexableUndefined(jinja2.Undefined):
--    def __getitem__(self, key):
--        return jinja2.Undefined(
--            hint=self._undefined_hint, obj=self._undefined_obj,
--            name=self._undefined_name, exc=self._undefined_exception)
--
--
--class IPAExtension(jinja2.ext.Extension):
--    """Jinja2 extension providing useful features for CSR generation rules."""
--
--    def __init__(self, environment):
--        super(IPAExtension, self).__init__(environment)
--
--        environment.filters.update(
--            quote=self.quote,
--            required=self.required,
--        )
--
--    def quote(self, data):
--        return pipes.quote(data)
--
--    def required(self, data, name):
--        if not data:
--            raise errors.CSRTemplateError(
--                reason=_(
--                    'Required CSR generation rule %(name)s is missing data') %
--                {'name': name})
--        return data
--
--
--class Formatter(object):
--    """
--    Class for processing a set of CSR generation rules into a template.
--
--    The template can be rendered with user and database data to produce a
--    config, which specifies how to build a CSR.
--
--    Subclasses of Formatter should set the value of base_template_name to the
--    filename of a base template with spaces for the processed rules.
--    Additionally, they should override the _get_template_params method to
--    produce the correct output for the base template.
--    """
--    base_template_name = None
--
--    def __init__(self, csr_data_dir=None):
--        # chain loaders:
--        # 1) csr_data_dir/templates
--        # 2) /etc/ipa/csrgen/templates
--        # 3) ipaclient/csrgen/templates
--        loaders = []
--        if csr_data_dir is not None:
--            loaders.append(jinja2.FileSystemLoader(
--                os.path.join(csr_data_dir, 'templates'))
--            )
--        loaders.append(jinja2.FileSystemLoader(
--            os.path.join(api.env.confdir, 'csrgen/templates'))
--        )
--        loaders.append(jinja2.PackageLoader('ipaclient', 'csrgen/templates'))
--
--        self.jinja2 = jinja2.sandbox.SandboxedEnvironment(
--            loader=jinja2.ChoiceLoader(loaders),
--            extensions=[jinja2.ext.ExprStmtExtension, IPAExtension],
--            keep_trailing_newline=True, undefined=IndexableUndefined)
--
--        self.passthrough_globals = {}
--
--    def _define_passthrough(self, call):
--        """Some macros are meant to be interpreted during the final render, not
--        when data rules are interpolated into syntax rules. This method allows
--        those macros to be registered so that calls to them are passed through
--        to the prepared rule rather than interpreted.
--        """
--
--        def passthrough(caller):
--            return u'{%% call %s() %%}%s{%% endcall %%}' % (call, caller())
--
--        parts = call.split('.')
--        current_level = self.passthrough_globals
--        for part in parts[:-1]:
--            if part not in current_level:
--                current_level[part] = {}
--            current_level = current_level[part]
--        current_level[parts[-1]] = passthrough
--
--    def build_template(self, rules):
--        """
--        Construct a template that can produce CSR generator strings.
--
--        :param rules: list of FieldMapping to use to populate the template.
--
--        :returns: jinja2.Template that can be rendered to produce the CSR data.
--        """
--        syntax_rules = []
--        for field_mapping in rules:
--            data_rules_prepared = [
--                self._prepare_data_rule(rule)
--                for rule in field_mapping.data_rules]
--
--            data_sources = []
--            for xrule in field_mapping.data_rules:
--                data_source = xrule.options.get('data_source')
--                if data_source:
--                    data_sources.append(data_source)
--
--            syntax_rules.append(self._prepare_syntax_rule(
--                field_mapping.syntax_rule, data_rules_prepared,
--                field_mapping.description, data_sources))
--
--        template_params = self._get_template_params(syntax_rules)
--        base_template = self.jinja2.get_template(
--            self.base_template_name, globals=self.passthrough_globals)
--
--        try:
--            combined_template_source = base_template.render(**template_params)
--        except jinja2.UndefinedError:
--            logger.debug(traceback.format_exc())
--            raise errors.CSRTemplateError(reason=_(
--                'Template error when formatting certificate data'))
--
--        logger.debug(
--            'Formatting with template: %s', combined_template_source)
--        combined_template = self.jinja2.from_string(combined_template_source)
--
--        return combined_template
--
--    def _wrap_conditional(self, rule, condition):
--        rule = '{%% if %s %%}%s{%% endif %%}' % (condition, rule)
--        return rule
--
--    def _wrap_required(self, rule, description):
--        template = '{%% filter required("%s") %%}%s{%% endfilter %%}' % (
--            description, rule)
--
--        return template
--
--    def _prepare_data_rule(self, data_rule):
--        template = data_rule.template
--
--        data_source = data_rule.options.get('data_source')
--        if data_source:
--            template = self._wrap_conditional(template, data_source)
--
--        return template
--
--    def _prepare_syntax_rule(
--            self, syntax_rule, data_rules, description, data_sources):
--        logger.debug('Syntax rule template: %s', syntax_rule.template)
--        template = self.jinja2.from_string(
--            syntax_rule.template, globals=self.passthrough_globals)
--        is_required = syntax_rule.options.get('required', False)
--        try:
--            prepared_template = template.render(datarules=data_rules)
--        except jinja2.UndefinedError:
--            logger.debug(traceback.format_exc())
--            raise errors.CSRTemplateError(reason=_(
--                'Template error when formatting certificate data'))
--
--        if data_sources:
--            combinator = ' %s ' % syntax_rule.options.get(
--                'data_source_combinator', 'or')
--            condition = combinator.join(data_sources)
--            prepared_template = self._wrap_conditional(
--                prepared_template, condition)
--
--        if is_required:
--            prepared_template = self._wrap_required(
--                prepared_template, description)
--
--        return prepared_template
--
--    def _get_template_params(self, syntax_rules):
--        """
--        Package the syntax rules into fields expected by the base template.
--
--        :param syntax_rules: list of prepared syntax rules to be included in
--            the template.
--
--        :returns: dict of values needed to render the base template.
--        """
--        raise NotImplementedError('Formatter class must be subclassed')
--
--
--class OpenSSLFormatter(Formatter):
--    """Formatter class generating the openssl config-file format."""
--
--    base_template_name = 'openssl_base.tmpl'
--
--    # Syntax rules are wrapped in this data structure, to keep track of whether
--    # each goes in the extension or the root section
--    SyntaxRule = collections.namedtuple(
--        'SyntaxRule', ['template', 'is_extension'])
--
--    def __init__(self, *args, **kwargs):
--        super(OpenSSLFormatter, self).__init__(*args, **kwargs)
--        self._define_passthrough('openssl.section')
--
--    def _get_template_params(self, syntax_rules):
--        parameters = [rule.template for rule in syntax_rules
--                      if not rule.is_extension]
--        extensions = [rule.template for rule in syntax_rules
--                      if rule.is_extension]
--
--        return {'parameters': parameters, 'extensions': extensions}
--
--    def _prepare_syntax_rule(
--            self, syntax_rule, data_rules, description, data_sources):
--        """Overrides method to pull out whether rule is an extension or not."""
--        prepared_template = super(OpenSSLFormatter, self)._prepare_syntax_rule(
--            syntax_rule, data_rules, description, data_sources)
--        is_extension = syntax_rule.options.get('extension', False)
--        return self.SyntaxRule(prepared_template, is_extension)
--
--
--class FieldMapping(object):
--    """Representation of the rules needed to construct a complete cert field.
--
--    Attributes:
--        description: str, a name or description of this field, to be used in
--            messages
--        syntax_rule: Rule, the rule defining the syntax of this field
--        data_rules: list of Rule, the rules that produce data to be stored in
--            this field
--    """
--    __slots__ = ['description', 'syntax_rule', 'data_rules']
--
--    def __init__(self, description, syntax_rule, data_rules):
--        self.description = description
--        self.syntax_rule = syntax_rule
--        self.data_rules = data_rules
--
--
--class Rule(object):
--    __slots__ = ['name', 'template', 'options']
--
--    def __init__(self, name, template, options):
--        self.name = name
--        self.template = template
--        self.options = options
--
--
--class RuleProvider(object):
--    def rules_for_profile(self, profile_id):
--        """
--        Return the rules needed to build a CSR using the given profile.
--
--        :param profile_id: str, name of the CSR generation profile to use
--
--        :returns: list of FieldMapping, filled out with the appropriate rules
--        """
--        raise NotImplementedError('RuleProvider class must be subclassed')
--
--
--class FileRuleProvider(RuleProvider):
--    def __init__(self, csr_data_dir=None):
--        self.rules = {}
--        self._csrgen_data_dirs = []
--        if csr_data_dir is not None:
--            self._csrgen_data_dirs.append(csr_data_dir)
--        self._csrgen_data_dirs.append(
--            os.path.join(api.env.confdir, 'csrgen')
--        )
--        self._csrgen_data_dirs.append(
--            pkg_resources.resource_filename('ipaclient', 'csrgen')
--        )
--
--    def _open(self, subdir, filename):
--        for data_dir in self._csrgen_data_dirs:
--            path = os.path.join(data_dir, subdir, filename)
--            try:
--                return open(path)
--            except IOError as e:
--                if e.errno != errno.ENOENT:
--                    raise
--        raise IOError(
--            errno.ENOENT,
--            "'{}' not found in {}".format(
--                os.path.join(subdir, filename),
--                ", ".join(self._csrgen_data_dirs)
--            )
--        )
--
--    def _rule(self, rule_name):
--        if rule_name not in self.rules:
--            try:
--                with self._open('rules', '%s.json' % rule_name) as f:
--                    ruleconf = json.load(f)
--            except IOError:
--                raise errors.NotFound(
--                    reason=_('No generation rule %(rulename)s found.') %
--                    {'rulename': rule_name})
--
--            try:
--                rule = ruleconf['rule']
--            except KeyError:
--                raise errors.EmptyResult(
--                    reason=_('Generation rule "%(rulename)s" is missing the'
--                             ' "rule" key') % {'rulename': rule_name})
--
--            options = ruleconf.get('options', {})
--
--            self.rules[rule_name] = Rule(
--                rule_name, rule['template'], options)
--
--        return self.rules[rule_name]
--
--    def rules_for_profile(self, profile_id):
--        try:
--            with self._open('profiles', '%s.json' % profile_id) as f:
--                profile = json.load(f)
--        except IOError:
--            raise errors.NotFound(
--                reason=_('No CSR generation rules are defined for profile'
--                         ' %(profile_id)s') % {'profile_id': profile_id})
--
--        field_mappings = []
--        for field in profile:
--            syntax_rule = self._rule(field['syntax'])
--            data_rules = [self._rule(name) for name in field['data']]
--            field_mappings.append(FieldMapping(
--                syntax_rule.name, syntax_rule, data_rules))
--        return field_mappings
--
--
--class CSRGenerator(object):
--    def __init__(self, rule_provider, formatter_class=OpenSSLFormatter):
--        self.rule_provider = rule_provider
--        self.formatter = formatter_class()
--
--    def csr_config(self, principal, config, profile_id):
--        render_data = {'subject': principal, 'config': config}
--
--        rules = self.rule_provider.rules_for_profile(profile_id)
--        template = self.formatter.build_template(rules)
--
--        try:
--            config = template.render(render_data)
--        except jinja2.UndefinedError:
--            logger.debug(traceback.format_exc())
--            raise errors.CSRTemplateError(reason=_(
--                'Template error when formatting certificate data'))
--
--        return config
--
--
--class CSRLibraryAdaptor(object):
--    def get_subject_public_key_info(self):
--        raise NotImplementedError('Use a subclass of CSRLibraryAdaptor')
--
--    def sign_csr(self, certification_request_info):
--        """Sign a CertificationRequestInfo.
--
--        :returns: bytes, a DER-encoded signed CSR.
--        """
--        raise NotImplementedError('Use a subclass of CSRLibraryAdaptor')
--
--
--class OpenSSLAdaptor(object):
--    def __init__(self, key=None, key_filename=None, password_filename=None):
--        """
--        Must provide either ``key_filename`` or ``key``.
--
--        """
--        if key_filename is not None:
--            with open(key_filename, 'rb') as key_file:
--                key_bytes = key_file.read()
--
--            password = None
--            if password_filename is not None:
--                with open(password_filename, 'rb') as password_file:
--                    password = password_file.read().strip()
--
--            self._key = load_pem_private_key(
--                key_bytes, password, default_backend())
--
--        elif key is not None:
--            self._key = key
--
--        else:
--            raise ValueError("Must provide 'key' or 'key_filename'")
--
--    def key(self):
--        return self._key
--
--    def get_subject_public_key_info(self):
--        pubkey_info = self.key().public_key().public_bytes(
--            Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
--        return pubkey_info
--
--    def sign_csr(self, certification_request_info):
--        reqinfo = decoder.decode(
--            certification_request_info, rfc2314.CertificationRequestInfo())[0]
--        csr = rfc2314.CertificationRequest()
--        csr.setComponentByName('certificationRequestInfo', reqinfo)
--
--        algorithm = rfc2314.SignatureAlgorithmIdentifier()
--        algorithm.setComponentByName(
--            'algorithm', univ.ObjectIdentifier(
--                '1.2.840.113549.1.1.11'))  # sha256WithRSAEncryption
--        csr.setComponentByName('signatureAlgorithm', algorithm)
--
--        signature = self.key().sign(
--            certification_request_info,
--            padding.PKCS1v15(),
--            hashes.SHA256()
--        )
--        asn1sig = univ.BitString("'{sig}'H".format(
--                                    sig=codecs.encode(signature, 'hex')
--                                    .decode('ascii'))
--                                 )
--        csr.setComponentByName('signature', asn1sig)
--        return encoder.encode(csr)
--
--
--class NSSAdaptor(object):
--    def __init__(self, database, password_filename):
--        self.database = database
--        self.password_filename = password_filename
--        self.nickname = base64.b32encode(os.urandom(40))
--
--    def get_subject_public_key_info(self):
--        temp_cn = base64.b32encode(os.urandom(40)).decode('ascii')
--
--        password_args = []
--        if self.password_filename is not None:
--            password_args = ['-f', self.password_filename]
--
--        subprocess.check_call(
--            ['certutil', '-S', '-n', self.nickname, '-s', 'CN=%s' % temp_cn,
--             '-x', '-t', ',,', '-d', self.database] + password_args)
--        cert_pem = subprocess.check_output(
--            ['certutil', '-L', '-n', self.nickname, '-a',
--             '-d', self.database] + password_args)
--
--        cert = load_pem_x509_certificate(cert_pem, default_backend())
--        pubkey_info = cert.public_key().public_bytes(
--            Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
--
--        return pubkey_info
--
--    def sign_csr(self, certification_request_info):
--        raise NotImplementedError('NSS is not yet supported')
-diff --git a/ipaclient/csrgen/profiles/caIPAserviceCert.json b/ipaclient/csrgen/profiles/caIPAserviceCert.json
-deleted file mode 100644
-index 114d2ff..0000000
---- a/ipaclient/csrgen/profiles/caIPAserviceCert.json
-+++ /dev/null
-@@ -1,15 +0,0 @@
--[
--    {
--        "syntax": "syntaxSubject",
--        "data": [
--            "dataHostCN",
--            "dataSubjectBase"
--        ]
--    },
--    {
--        "syntax": "syntaxSAN",
--        "data": [
--            "dataDNS"
--        ]
--    }
--]
-diff --git a/ipaclient/csrgen/profiles/userCert.json b/ipaclient/csrgen/profiles/userCert.json
-deleted file mode 100644
-index d6cf5cf..0000000
---- a/ipaclient/csrgen/profiles/userCert.json
-+++ /dev/null
-@@ -1,15 +0,0 @@
--[
--    {
--        "syntax": "syntaxSubject",
--        "data": [
--            "dataUsernameCN",
--            "dataSubjectBase"
--        ]
--    },
--    {
--        "syntax": "syntaxSAN",
--        "data": [
--            "dataEmail"
--        ]
--    }
--]
-diff --git a/ipaclient/csrgen/rules/dataDNS.json b/ipaclient/csrgen/rules/dataDNS.json
-deleted file mode 100644
-index a79a3d7..0000000
---- a/ipaclient/csrgen/rules/dataDNS.json
-+++ /dev/null
-@@ -1,8 +0,0 @@
--{
--  "rule": {
--    "template": "DNS = {{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}"
--  },
--  "options": {
--    "data_source": "subject.krbprincipalname.0.partition('/')[2].partition('@')[0]"
--  }
--}
-diff --git a/ipaclient/csrgen/rules/dataEmail.json b/ipaclient/csrgen/rules/dataEmail.json
-deleted file mode 100644
-index 4be6cec..0000000
---- a/ipaclient/csrgen/rules/dataEmail.json
-+++ /dev/null
-@@ -1,8 +0,0 @@
--{
--  "rule": {
--    "template": "email = {{subject.mail.0}}"
--  },
--  "options": {
--    "data_source": "subject.mail.0"
--  }
--}
-diff --git a/ipaclient/csrgen/rules/dataHostCN.json b/ipaclient/csrgen/rules/dataHostCN.json
-deleted file mode 100644
-index f30c50f..0000000
---- a/ipaclient/csrgen/rules/dataHostCN.json
-+++ /dev/null
-@@ -1,8 +0,0 @@
--{
--  "rule": {
--    "template": "CN={{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}"
--  },
--  "options": {
--    "data_source": "subject.krbprincipalname.0.partition('/')[2].partition('@')[0]"
--  }
--}
-diff --git a/ipaclient/csrgen/rules/dataSubjectBase.json b/ipaclient/csrgen/rules/dataSubjectBase.json
-deleted file mode 100644
-index 31a38b4..0000000
---- a/ipaclient/csrgen/rules/dataSubjectBase.json
-+++ /dev/null
-@@ -1,8 +0,0 @@
--{
--  "rule": {
--    "template": "{{config.ipacertificatesubjectbase.0}}"
--  },
--  "options": {
--    "data_source": "config.ipacertificatesubjectbase.0"
--  }
--}
-diff --git a/ipaclient/csrgen/rules/dataUsernameCN.json b/ipaclient/csrgen/rules/dataUsernameCN.json
-deleted file mode 100644
-index acbb524..0000000
---- a/ipaclient/csrgen/rules/dataUsernameCN.json
-+++ /dev/null
-@@ -1,8 +0,0 @@
--{
--  "rule": {
--    "template": "CN={{subject.uid.0}}"
--  },
--  "options": {
--    "data_source": "subject.uid.0"
--  }
--}
-diff --git a/ipaclient/csrgen/rules/syntaxSAN.json b/ipaclient/csrgen/rules/syntaxSAN.json
-deleted file mode 100644
-index c6943ed..0000000
---- a/ipaclient/csrgen/rules/syntaxSAN.json
-+++ /dev/null
-@@ -1,8 +0,0 @@
--{
--  "rule": {
--    "template": "subjectAltName = @{% call openssl.section() %}{{ datarules|join('\n') }}{% endcall %}"
--  },
--  "options": {
--    "extension": true
--  }
--}
-diff --git a/ipaclient/csrgen/rules/syntaxSubject.json b/ipaclient/csrgen/rules/syntaxSubject.json
-deleted file mode 100644
-index c609e01..0000000
---- a/ipaclient/csrgen/rules/syntaxSubject.json
-+++ /dev/null
-@@ -1,9 +0,0 @@
--{
--  "rule": {
--    "template": "distinguished_name = {% call openssl.section() %}{{ datarules|reverse|join('\n') }}{% endcall %}"
--  },
--  "options": {
--    "required": true,
--    "data_source_combinator": "and"
--  }
--}
-diff --git a/ipaclient/csrgen/templates/openssl_base.tmpl b/ipaclient/csrgen/templates/openssl_base.tmpl
-deleted file mode 100644
-index 8d37994..0000000
---- a/ipaclient/csrgen/templates/openssl_base.tmpl
-+++ /dev/null
-@@ -1,17 +0,0 @@
--{% raw -%}
--{% import "openssl_macros.tmpl" as openssl -%}
--{% endraw -%}
--[ req ]
--prompt = no
--encrypt_key = no
--
--{{ parameters|join('\n') }}
--{% raw %}{% set rendered_extensions -%}{% endraw %}
--{{ extensions|join('\n') }}
--{% raw -%}
--{%- endset -%}
--{% if rendered_extensions -%}
--req_extensions = {% call openssl.section() %}{{ rendered_extensions }}{% endcall %}
--{% endif %}
--{{ openssl.openssl_sections|join('\n\n') }}
--{%- endraw %}
-diff --git a/ipaclient/csrgen/templates/openssl_macros.tmpl b/ipaclient/csrgen/templates/openssl_macros.tmpl
-deleted file mode 100644
-index d31b8fe..0000000
---- a/ipaclient/csrgen/templates/openssl_macros.tmpl
-+++ /dev/null
-@@ -1,29 +0,0 @@
--{# List containing rendered sections to be included at end #}
--{% set openssl_sections = [] %}
--
--{#
--List containing one entry for each section name allocated. Because of
--scoping rules, we need to use a list so that it can be a "per-render global"
--that gets updated in place. Real globals are shared by all templates with the
--same environment, and variables defined in the macro don't persist after the
--macro invocation ends.
--#}
--{% set openssl_section_num = [] %}
--
--{% macro section() -%}
--{% set name -%}
--sec{{ openssl_section_num|length -}}
--{% endset -%}
--{% do openssl_section_num.append('') -%}
--{% set contents %}{{ caller() }}{% endset -%}
--{% if contents -%}
--{% set sectiondata = formatsection(name, contents) -%}
--{% do openssl_sections.append(sectiondata) -%}
--{% endif -%}
--{{ name -}}
--{% endmacro %}
--
--{% macro formatsection(name, contents) -%}
--[ {{ name }} ]
--{{ contents -}}
--{% endmacro %}
-diff --git a/ipaclient/csrgen_ffi.py b/ipaclient/csrgen_ffi.py
-deleted file mode 100644
-index 10a20bd..0000000
---- a/ipaclient/csrgen_ffi.py
-+++ /dev/null
-@@ -1,331 +0,0 @@
--from cffi import FFI
--import ctypes.util
--
--from ipalib import errors
--
--_ffi = FFI()
--
--_ffi.cdef('''
--typedef ... CONF;
--typedef ... CONF_METHOD;
--typedef ... BIO;
--typedef ... ipa_STACK_OF_CONF_VALUE;
--
--/* openssl/conf.h */
--typedef struct {
--    char *section;
--    char *name;
--    char *value;
--} CONF_VALUE;
--
--CONF *NCONF_new(CONF_METHOD *meth);
--void NCONF_free(CONF *conf);
--int NCONF_load_bio(CONF *conf, BIO *bp, long *eline);
--ipa_STACK_OF_CONF_VALUE *NCONF_get_section(const CONF *conf,
--                                        const char *section);
--char *NCONF_get_string(const CONF *conf, const char *group, const char *name);
--
--/* openssl/safestack.h */
--// int sk_CONF_VALUE_num(ipa_STACK_OF_CONF_VALUE *);
--// CONF_VALUE *sk_CONF_VALUE_value(ipa_STACK_OF_CONF_VALUE *, int);
--
--/* openssl/stack.h */
--typedef ... _STACK;
--
--int OPENSSL_sk_num(const _STACK *);
--void *OPENSSL_sk_value(const _STACK *, int);
--
--int sk_num(const _STACK *);
--void *sk_value(const _STACK *, int);
--
--/* openssl/bio.h */
--BIO *BIO_new_mem_buf(const void *buf, int len);
--int BIO_free(BIO *a);
--
--/* openssl/asn1.h */
--typedef struct ASN1_ENCODING_st {
--    unsigned char *enc;         /* DER encoding */
--    long len;                   /* Length of encoding */
--    int modified;               /* set to 1 if 'enc' is invalid */
--} ASN1_ENCODING;
--
--/* openssl/evp.h */
--typedef ... EVP_PKEY;
--
--void EVP_PKEY_free(EVP_PKEY *pkey);
--
--/* openssl/x509.h */
--typedef ... ASN1_INTEGER;
--typedef ... ASN1_BIT_STRING;
--typedef ... ASN1_OBJECT;
--typedef ... X509;
--typedef ... X509_ALGOR;
--typedef ... X509_CRL;
--typedef ... X509_NAME;
--typedef ... X509_PUBKEY;
--typedef ... ipa_STACK_OF_X509_ATTRIBUTE;
--
--typedef struct X509_req_info_st {
--    ASN1_ENCODING enc;
--    ASN1_INTEGER *version;
--    X509_NAME *subject;
--    X509_PUBKEY *pubkey;
--    /*  d=2 hl=2 l=  0 cons: cont: 00 */
--    ipa_STACK_OF_X509_ATTRIBUTE *attributes; /* [ 0 ] */
--} X509_REQ_INFO;
--
--typedef struct X509_req_st {
--    X509_REQ_INFO *req_info;
--    X509_ALGOR *sig_alg;
--    ASN1_BIT_STRING *signature;
--    int references;
--} X509_REQ;
--
--X509_REQ *X509_REQ_new(void);
--void X509_REQ_free(X509_REQ *);
--EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a);
--int X509_REQ_set_pubkey(X509_REQ *x, EVP_PKEY *pkey);
--int X509_NAME_add_entry_by_OBJ(X509_NAME *name, const ASN1_OBJECT *obj, int type,
--                               const unsigned char *bytes, int len, int loc,
--                               int set);
--int X509_NAME_entry_count(X509_NAME *name);
--int i2d_X509_REQ_INFO(X509_REQ_INFO *a, unsigned char **out);
--
--/* openssl/objects.h */
--ASN1_OBJECT *OBJ_txt2obj(const char *s, int no_name);
--
--/* openssl/x509v3.h */
--typedef ... X509V3_CONF_METHOD;
--
--typedef struct v3_ext_ctx {
--    int flags;
--    X509 *issuer_cert;
--    X509 *subject_cert;
--    X509_REQ *subject_req;
--    X509_CRL *crl;
--    X509V3_CONF_METHOD *db_meth;
--    void *db;
--} X509V3_CTX;
--
--void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject,
--                    X509_REQ *req, X509_CRL *crl, int flags);
--void X509V3_set_nconf(X509V3_CTX *ctx, CONF *conf);
--int X509V3_EXT_REQ_add_nconf(CONF *conf, X509V3_CTX *ctx, char *section,
--                             X509_REQ *req);
--
--/* openssl/x509v3.h */
--unsigned long ERR_get_error(void);
--char *ERR_error_string(unsigned long e, char *buf);
--''')  # noqa: E501
--
--_libcrypto = _ffi.dlopen(ctypes.util.find_library('crypto'))
--
--NULL = _ffi.NULL
--
--# openssl/conf.h
--NCONF_new = _libcrypto.NCONF_new
--NCONF_free = _libcrypto.NCONF_free
--NCONF_load_bio = _libcrypto.NCONF_load_bio
--NCONF_get_section = _libcrypto.NCONF_get_section
--NCONF_get_string = _libcrypto.NCONF_get_string
--
--# openssl/stack.h
--try:
--    sk_num = _libcrypto.OPENSSL_sk_num
--    sk_value = _libcrypto.OPENSSL_sk_value
--except AttributeError:
--    sk_num = _libcrypto.sk_num
--    sk_value = _libcrypto.sk_value
--
--
--def sk_CONF_VALUE_num(sk):
--    return sk_num(_ffi.cast("_STACK *", sk))
--
--
--def sk_CONF_VALUE_value(sk, i):
--    return _ffi.cast("CONF_VALUE *", sk_value(_ffi.cast("_STACK *", sk), i))
--
--
--# openssl/bio.h
--BIO_new_mem_buf = _libcrypto.BIO_new_mem_buf
--BIO_free = _libcrypto.BIO_free
--
--# openssl/x509.h
--X509_REQ_new = _libcrypto.X509_REQ_new
--X509_REQ_free = _libcrypto.X509_REQ_free
--X509_REQ_set_pubkey = _libcrypto.X509_REQ_set_pubkey
--d2i_PUBKEY_bio = _libcrypto.d2i_PUBKEY_bio
--i2d_X509_REQ_INFO = _libcrypto.i2d_X509_REQ_INFO
--X509_NAME_add_entry_by_OBJ = _libcrypto.X509_NAME_add_entry_by_OBJ
--X509_NAME_entry_count = _libcrypto.X509_NAME_entry_count
--
--
--def X509_REQ_get_subject_name(req):
--    return req.req_info.subject
--
--
--# openssl/objects.h
--OBJ_txt2obj = _libcrypto.OBJ_txt2obj
--
--# openssl/evp.h
--EVP_PKEY_free = _libcrypto.EVP_PKEY_free
--
--# openssl/asn1.h
--MBSTRING_UTF8 = 0x1000
--
--# openssl/x509v3.h
--X509V3_set_ctx = _libcrypto.X509V3_set_ctx
--X509V3_set_nconf = _libcrypto.X509V3_set_nconf
--X509V3_EXT_REQ_add_nconf = _libcrypto.X509V3_EXT_REQ_add_nconf
--
--# openssl/err.h
--ERR_get_error = _libcrypto.ERR_get_error
--ERR_error_string = _libcrypto.ERR_error_string
--
--
--def _raise_openssl_errors():
--    msgs = []
--
--    code = ERR_get_error()
--    while code != 0:
--        msg = _ffi.string(ERR_error_string(code, NULL))
--        try:
--            strmsg = msg.decode('utf-8')
--        except UnicodeDecodeError:
--            strmsg = repr(msg)
--        msgs.append(strmsg)
--        code = ERR_get_error()
--
--    raise errors.CSRTemplateError(reason='\n'.join(msgs))
--
--
--def _parse_dn_section(subj, dn_sk):
--    for i in range(sk_CONF_VALUE_num(dn_sk)):
--        v = sk_CONF_VALUE_value(dn_sk, i)
--        rdn_type = _ffi.string(v.name)
--
--        # Skip past any leading X. X: X, etc to allow for multiple instances
--        for idx, c in enumerate(rdn_type):
--            if c in b':,.':
--                if idx+1 < len(rdn_type):
--                    rdn_type = rdn_type[idx+1:]
--                break
--        if rdn_type.startswith(b'+'):
--            rdn_type = rdn_type[1:]
--            mval = -1
--        else:
--            mval = 0
--
--        # convert rdn_type to an OID
--        #
--        # OpenSSL is fussy about the case of the string.  For example,
--        # lower-case 'o' (for "organization name") is not recognised.
--        # Therefore, try to convert the given string into an OID.  If
--        # that fails, convert it upper case and try again.
--        #
--        oid = OBJ_txt2obj(rdn_type, 0)
--        if oid == NULL:
--            oid = OBJ_txt2obj(rdn_type.upper(), 0)
--        if oid == NULL:
--            raise errors.CSRTemplateError(
--                reason='unrecognised attribute type: {}'
--                .format(rdn_type.decode('utf-8')))
--
--        if not X509_NAME_add_entry_by_OBJ(
--                subj, oid, MBSTRING_UTF8,
--                _ffi.cast("unsigned char *", v.value), -1, -1, mval):
--            _raise_openssl_errors()
--
--    if not X509_NAME_entry_count(subj):
--        raise errors.CSRTemplateError(
--            reason='error, subject in config file is empty')
--
--
--def build_requestinfo(config, public_key_info):
--    '''
--    Return a cffi buffer containing a DER-encoded CertificationRequestInfo.
--
--    The returned object implements the buffer protocol.
--
--    '''
--    reqdata = NULL
--    req = NULL
--    nconf_bio = NULL
--    pubkey_bio = NULL
--    pubkey = NULL
--
--    try:
--        reqdata = NCONF_new(NULL)
--        if reqdata == NULL:
--            _raise_openssl_errors()
--
--        nconf_bio = BIO_new_mem_buf(config, len(config))
--        errorline = _ffi.new('long[1]', [-1])
--        i = NCONF_load_bio(reqdata, nconf_bio, errorline)
--        if i < 0:
--            if errorline[0] < 0:
--                raise errors.CSRTemplateError(reason="Can't load config file")
--            else:
--                raise errors.CSRTemplateError(
--                    reason='Error on line %d of config file' % errorline[0])
--
--        dn_sect = NCONF_get_string(reqdata, b'req', b'distinguished_name')
--        if dn_sect == NULL:
--            raise errors.CSRTemplateError(
--                reason='Unable to find "distinguished_name" key in config')
--
--        dn_sk = NCONF_get_section(reqdata, dn_sect)
--        if dn_sk == NULL:
--            raise errors.CSRTemplateError(
--                reason='Unable to find "%s" section in config' %
--                _ffi.string(dn_sect))
--
--        pubkey_bio = BIO_new_mem_buf(public_key_info, len(public_key_info))
--        pubkey = d2i_PUBKEY_bio(pubkey_bio, NULL)
--        if pubkey == NULL:
--            _raise_openssl_errors()
--
--        req = X509_REQ_new()
--        if req == NULL:
--            _raise_openssl_errors()
--
--        subject = X509_REQ_get_subject_name(req)
--
--        _parse_dn_section(subject, dn_sk)
--
--        if not X509_REQ_set_pubkey(req, pubkey):
--            _raise_openssl_errors()
--
--        ext_ctx = _ffi.new("X509V3_CTX[1]")
--        X509V3_set_ctx(ext_ctx, NULL, NULL, req, NULL, 0)
--        X509V3_set_nconf(ext_ctx, reqdata)
--
--        extn_section = NCONF_get_string(reqdata, b"req", b"req_extensions")
--        if extn_section != NULL:
--            if not X509V3_EXT_REQ_add_nconf(
--                    reqdata, ext_ctx, extn_section, req):
--                _raise_openssl_errors()
--
--        der_len = i2d_X509_REQ_INFO(req.req_info, NULL)
--        if der_len < 0:
--            _raise_openssl_errors()
--
--        der_buf = _ffi.new("unsigned char[%d]" % der_len)
--        der_out = _ffi.new("unsigned char **", der_buf)
--        der_len = i2d_X509_REQ_INFO(req.req_info, der_out)
--        if der_len < 0:
--            _raise_openssl_errors()
--
--        return _ffi.buffer(der_buf, der_len)
--
--    finally:
--        if reqdata != NULL:
--            NCONF_free(reqdata)
--        if req != NULL:
--            X509_REQ_free(req)
--        if nconf_bio != NULL:
--            BIO_free(nconf_bio)
--        if pubkey_bio != NULL:
--            BIO_free(pubkey_bio)
--        if pubkey != NULL:
--            EVP_PKEY_free(pubkey)
-diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
-index 6d453d0..8a4db81 100644
---- a/ipaclient/plugins/cert.py
-+++ b/ipaclient/plugins/cert.py
-@@ -21,8 +21,6 @@
- 
- import base64
- 
--import six
--
- from ipaclient.frontend import MethodOverride
- from ipalib import errors
- from ipalib import x509
-@@ -31,9 +29,6 @@ from ipalib.parameters import BinaryFile, File, Flag, Str
- from ipalib.plugable import Registry
- from ipalib.text import _
- 
--if six.PY3:
--    unicode = str
--
- register = Registry()
- 
- 
-@@ -74,87 +69,12 @@ class CertRetrieveOverride(MethodOverride):
- 
- @register(override=True, no_fail=True)
- class cert_request(CertRetrieveOverride):
--    takes_options = CertRetrieveOverride.takes_options + (
--        Str(
--            'database?',
--            label=_('Path to NSS database'),
--            doc=_('Path to NSS database to use for private key'),
--        ),
--        Str(
--            'private_key?',
--            label=_('Path to private key file'),
--            doc=_('Path to PEM file containing a private key'),
--        ),
--        Str(
--            'password_file?',
--            label=_(
--                'File containing a password for the private key or database'),
--        ),
--        Str(
--            'csr_profile_id?',
--            label=_('Name of CSR generation profile (if not the same as'
--                    ' profile_id)'),
--        ),
--    )
--
-     def get_args(self):
-         for arg in super(cert_request, self).get_args():
-             if arg.name == 'csr':
-                 arg = arg.clone_retype(arg.name, File, required=False)
-             yield arg
- 
--    def forward(self, csr=None, **options):
--        database = options.pop('database', None)
--        private_key = options.pop('private_key', None)
--        csr_profile_id = options.pop('csr_profile_id', None)
--        password_file = options.pop('password_file', None)
--
--        if csr is None:
--            # Deferred import, ipaclient.csrgen is expensive to load.
--            # see https://pagure.io/freeipa/issue/7484
--            from ipaclient import csrgen
--
--            if database:
--                adaptor = csrgen.NSSAdaptor(database, password_file)
--            elif private_key:
--                adaptor = csrgen.OpenSSLAdaptor(
--                    key_filename=private_key, password_filename=password_file)
--            else:
--                raise errors.InvocationError(
--                    message=u"One of 'database' or 'private_key' is required")
--
--            pubkey_info = adaptor.get_subject_public_key_info()
--            pubkey_info_b64 = base64.b64encode(pubkey_info)
--
--            # If csr_profile_id is passed, that takes precedence.
--            # Otherwise, use profile_id. If neither are passed, the default
--            # in cert_get_requestdata will be used.
--            profile_id = csr_profile_id
--            if profile_id is None:
--                profile_id = options.get('profile_id')
--
--            response = self.api.Command.cert_get_requestdata(
--                profile_id=profile_id,
--                principal=options.get('principal'),
--                public_key_info=pubkey_info_b64)
--
--            req_info_b64 = response['result']['request_info']
--            req_info = base64.b64decode(req_info_b64)
--
--            csr = adaptor.sign_csr(req_info)
--
--            if not csr:
--                raise errors.CertificateOperationError(
--                    error=(_('Generated CSR was empty')))
--
--        else:
--            if database is not None or private_key is not None:
--                raise errors.MutuallyExclusiveError(reason=_(
--                    "Options 'database' and 'private_key' are not compatible"
--                    " with 'csr'"))
--
--        return super(cert_request, self).forward(csr, **options)
--
- 
- @register(override=True, no_fail=True)
- class cert_show(CertRetrieveOverride):
-diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py
-deleted file mode 100644
-index 5aad636..0000000
---- a/ipaclient/plugins/csrgen.py
-+++ /dev/null
-@@ -1,128 +0,0 @@
--#
--# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
--#
--
--import base64
--
--import six
--
--from ipalib import api
--from ipalib import errors
--from ipalib import output
--from ipalib import util
--from ipalib.frontend import Local, Str
--from ipalib.parameters import Bytes, Principal
--from ipalib.plugable import Registry
--from ipalib.text import _
--from ipapython import dogtag
--
--
--if six.PY3:
--    unicode = str
--
--register = Registry()
--
--__doc__ = _("""
--Commands to build certificate requests automatically
--""")
--
--
--@register()
--class cert_get_requestdata(Local):
--    __doc__ = _('Gather data for a certificate signing request.')
--
--    NO_CLI = True
--
--    takes_options = (
--        Principal(
--            'principal',
--            label=_('Principal'),
--            doc=_('Principal for this certificate (e.g.'
--                  ' HTTP/test.example.com)'),
--        ),
--        Str(
--            'profile_id?',
--            label=_('Profile ID'),
--            doc=_('CSR Generation Profile to use'),
--        ),
--        Bytes(
--            'public_key_info',
--            label=_('Subject Public Key Info'),
--            doc=_('DER-encoded SubjectPublicKeyInfo structure'),
--        ),
--        Str(
--            'out?',
--            doc=_('Write CertificationRequestInfo to file'),
--        ),
--    )
--
--    has_output = (
--        output.Output(
--            'result',
--            type=dict,
--            doc=_('Dictionary mapping variable name to value'),
--        ),
--    )
--
--    has_output_params = (
--        Str(
--            'request_info',
--            label=_('CertificationRequestInfo structure'),
--        )
--    )
--
--    def execute(self, *args, **options):
--        # Deferred import, ipaclient.csrgen is expensive to load.
--        # see https://pagure.io/freeipa/issue/7484
--        from ipaclient import csrgen
--        from ipaclient import csrgen_ffi
--
--        if 'out' in options:
--            util.check_writable_file(options['out'])
--
--        principal = options.get('principal')
--        profile_id = options.get('profile_id')
--        if profile_id is None:
--            profile_id = dogtag.DEFAULT_PROFILE
--        public_key_info = options.get('public_key_info')
--        public_key_info = base64.b64decode(public_key_info)
--
--        if self.api.env.in_server:
--            backend = self.api.Backend.ldap2
--        else:
--            backend = self.api.Backend.rpcclient
--        if not backend.isconnected():
--            backend.connect()
--
--        try:
--            if principal.is_host:
--                principal_obj = api.Command.host_show(
--                    principal.hostname, all=True)
--            elif principal.is_service:
--                principal_obj = api.Command.service_show(
--                    unicode(principal), all=True)
--            elif principal.is_user:
--                principal_obj = api.Command.user_show(
--                    principal.username, all=True)
--        except errors.NotFound:
--            raise errors.NotFound(
--                reason=_("The principal for this request doesn't exist."))
--        principal_obj = principal_obj['result']
--        config = api.Command.config_show()['result']
--
--        generator = csrgen.CSRGenerator(csrgen.FileRuleProvider())
--
--        csr_config = generator.csr_config(principal_obj, config, profile_id)
--        request_info = base64.b64encode(csrgen_ffi.build_requestinfo(
--            csr_config.encode('utf8'), public_key_info))
--
--        result = {}
--        if 'out' in options:
--            with open(options['out'], 'wb') as f:
--                f.write(request_info)
--        else:
--            result = dict(request_info=request_info)
--
--        return dict(
--            result=result
--        )
-diff --git a/ipaclient/setup.py b/ipaclient/setup.py
-index 8eb9aee..cd03685 100644
---- a/ipaclient/setup.py
-+++ b/ipaclient/setup.py
-@@ -41,13 +41,6 @@ if __name__ == '__main__':
-             "ipaclient.remote_plugins.2_156",
-             "ipaclient.remote_plugins.2_164",
-         ],
--        package_data={
--            'ipaclient': [
--                'csrgen/profiles/*.json',
--                'csrgen/rules/*.json',
--                'csrgen/templates/*.tmpl',
--            ],
--        },
-         install_requires=[
-             "cryptography",
-             "ipalib",
-@@ -63,7 +56,6 @@ if __name__ == '__main__':
-         extras_require={
-             "install": ["ipaplatform"],
-             "otptoken_yubikey": ["python-yubico", "pyusb"],
--            "csrgen": ["cffi", "jinja2"],
-         },
-         zip_safe=False,
-     )
-diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
-deleted file mode 100644
-index 3724bdc..0000000
---- a/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
-+++ /dev/null
-@@ -1,16 +0,0 @@
--[ req ]
--prompt = no
--encrypt_key = no
--
--distinguished_name = sec0
--req_extensions = sec2
--
--[ sec0 ]
--O=DOMAIN.EXAMPLE.COM
--CN=machine.example.com
--
--[ sec1 ]
--DNS = machine.example.com
--
--[ sec2 ]
--subjectAltName = @sec1
-diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
-deleted file mode 100644
-index 00d63de..0000000
---- a/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
-+++ /dev/null
-@@ -1,16 +0,0 @@
--[ req ]
--prompt = no
--encrypt_key = no
--
--distinguished_name = sec0
--req_extensions = sec2
--
--[ sec0 ]
--O=DOMAIN.EXAMPLE.COM
--CN=testuser
--
--[ sec1 ]
--email = testuser@example.com
--
--[ sec2 ]
--subjectAltName = @sec1
-diff --git a/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json b/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json
-deleted file mode 100644
-index 676f91b..0000000
---- a/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json
-+++ /dev/null
-@@ -1,8 +0,0 @@
--[
--    {
--        "syntax": "basic",
--        "data": [
--            "options"
--        ]
--    }
--]
-diff --git a/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json b/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json
-deleted file mode 100644
-index 094ef71..0000000
---- a/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json
-+++ /dev/null
-@@ -1,5 +0,0 @@
--{
--  "rule": {
--    "template": "openssl_rule"
--  }
--}
-diff --git a/ipatests/test_ipaclient/data/test_csrgen/rules/options.json b/ipatests/test_ipaclient/data/test_csrgen/rules/options.json
-deleted file mode 100644
-index 393ed8c..0000000
---- a/ipatests/test_ipaclient/data/test_csrgen/rules/options.json
-+++ /dev/null
-@@ -1,8 +0,0 @@
--{
--  "rule": {
--    "template": "openssl_rule"
--  },
--  "options": {
--    "rule_option": true
--  }
--}
-diff --git a/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl b/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl
-deleted file mode 100644
-index 79111ab..0000000
---- a/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl
-+++ /dev/null
-@@ -1 +0,0 @@
--{{ options|join(";") }}
-diff --git a/ipatests/test_ipaclient/test_csrgen.py b/ipatests/test_ipaclient/test_csrgen.py
-deleted file mode 100644
-index b58596b..0000000
---- a/ipatests/test_ipaclient/test_csrgen.py
-+++ /dev/null
-@@ -1,304 +0,0 @@
--#
--# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
--#
--
--import os
--import pytest
--
--from cryptography.hazmat.backends import default_backend
--from cryptography.hazmat.primitives.asymmetric import rsa
--from cryptography import x509
--
--from ipaclient import csrgen, csrgen_ffi
--from ipalib import errors
--
--BASE_DIR = os.path.dirname(__file__)
--CSR_DATA_DIR = os.path.join(BASE_DIR, 'data', 'test_csrgen')
--
--
--@pytest.fixture
--def formatter():
--    return csrgen.Formatter(csr_data_dir=CSR_DATA_DIR)
--
--
--@pytest.fixture
--def rule_provider():
--    return csrgen.FileRuleProvider(csr_data_dir=CSR_DATA_DIR)
--
--
--@pytest.fixture
--def generator():
--    return csrgen.CSRGenerator(csrgen.FileRuleProvider())
--
--
--class StubRuleProvider(csrgen.RuleProvider):
--    def __init__(self):
--        self.syntax_rule = csrgen.Rule(
--            'syntax', '{{datarules|join(",")}}', {})
--        self.data_rule = csrgen.Rule('data', 'data_template', {})
--        self.field_mapping = csrgen.FieldMapping(
--            'example', self.syntax_rule, [self.data_rule])
--        self.rules = [self.field_mapping]
--
--    def rules_for_profile(self, profile_id):
--        return self.rules
--
--
--class IdentityFormatter(csrgen.Formatter):
--    base_template_name = 'identity_base.tmpl'
--
--    def __init__(self):
--        super(IdentityFormatter, self).__init__(csr_data_dir=CSR_DATA_DIR)
--
--    def _get_template_params(self, syntax_rules):
--        return {'options': syntax_rules}
--
--
--class test_Formatter(object):
--    def test_prepare_data_rule_with_data_source(self, formatter):
--        data_rule = csrgen.Rule('uid', '{{subject.uid.0}}',
--                                {'data_source': 'subject.uid.0'})
--        prepared = formatter._prepare_data_rule(data_rule)
--        assert prepared == '{% if subject.uid.0 %}{{subject.uid.0}}{% endif %}'
--
--    def test_prepare_data_rule_no_data_source(self, formatter):
--        """Not a normal case, but we should handle it anyway"""
--        data_rule = csrgen.Rule('uid', 'static_text', {})
--        prepared = formatter._prepare_data_rule(data_rule)
--        assert prepared == 'static_text'
--
--    def test_prepare_syntax_rule_with_data_sources(self, formatter):
--        syntax_rule = csrgen.Rule(
--            'example', '{{datarules|join(",")}}', {})
--        data_rules = ['{{subject.field1}}', '{{subject.field2}}']
--        data_sources = ['subject.field1', 'subject.field2']
--        prepared = formatter._prepare_syntax_rule(
--            syntax_rule, data_rules, 'example', data_sources)
--
--        assert prepared == (
--            '{% if subject.field1 or subject.field2 %}{{subject.field1}},'
--            '{{subject.field2}}{% endif %}')
--
--    def test_prepare_syntax_rule_with_combinator(self, formatter):
--        syntax_rule = csrgen.Rule('example', '{{datarules|join(",")}}',
--                                  {'data_source_combinator': 'and'})
--        data_rules = ['{{subject.field1}}', '{{subject.field2}}']
--        data_sources = ['subject.field1', 'subject.field2']
--        prepared = formatter._prepare_syntax_rule(
--            syntax_rule, data_rules, 'example', data_sources)
--
--        assert prepared == (
--            '{% if subject.field1 and subject.field2 %}{{subject.field1}},'
--            '{{subject.field2}}{% endif %}')
--
--    def test_prepare_syntax_rule_required(self, formatter):
--        syntax_rule = csrgen.Rule('example', '{{datarules|join(",")}}',
--                                  {'required': True})
--        data_rules = ['{{subject.field1}}']
--        data_sources = ['subject.field1']
--        prepared = formatter._prepare_syntax_rule(
--            syntax_rule, data_rules, 'example', data_sources)
--
--        assert prepared == (
--            '{% filter required("example") %}{% if subject.field1 %}'
--            '{{subject.field1}}{% endif %}{% endfilter %}')
--
--    def test_prepare_syntax_rule_passthrough(self, formatter):
--        """
--        Calls to macros defined as passthrough are still call tags in the final
--        template.
--        """
--        formatter._define_passthrough('example.macro')
--
--        syntax_rule = csrgen.Rule(
--            'example',
--            '{% call example.macro() %}{{datarules|join(",")}}{% endcall %}',
--            {})
--        data_rules = ['{{subject.field1}}']
--        data_sources = ['subject.field1']
--        prepared = formatter._prepare_syntax_rule(
--            syntax_rule, data_rules, 'example', data_sources)
--
--        assert prepared == (
--            '{% if subject.field1 %}{% call example.macro() %}'
--            '{{subject.field1}}{% endcall %}{% endif %}')
--
--    def test_prepare_syntax_rule_no_data_sources(self, formatter):
--        """Not a normal case, but we should handle it anyway"""
--        syntax_rule = csrgen.Rule(
--            'example', '{{datarules|join(",")}}', {})
--        data_rules = ['rule1', 'rule2']
--        data_sources = []
--        prepared = formatter._prepare_syntax_rule(
--            syntax_rule, data_rules, 'example', data_sources)
--
--        assert prepared == 'rule1,rule2'
--
--
--class test_FileRuleProvider(object):
--    def test_rule_basic(self, rule_provider):
--        rule_name = 'basic'
--
--        rule = rule_provider._rule(rule_name)
--
--        assert rule.template == 'openssl_rule'
--
--    def test_rule_global_options(self, rule_provider):
--        rule_name = 'options'
--
--        rule = rule_provider._rule(rule_name)
--
--        assert rule.options['rule_option'] is True
--
--    def test_rule_nosuchrule(self, rule_provider):
--        with pytest.raises(errors.NotFound):
--            rule_provider._rule('nosuchrule')
--
--    def test_rules_for_profile_success(self, rule_provider):
--        rules = rule_provider.rules_for_profile('profile')
--
--        assert len(rules) == 1
--        field_mapping = rules[0]
--        assert field_mapping.syntax_rule.name == 'basic'
--        assert len(field_mapping.data_rules) == 1
--        assert field_mapping.data_rules[0].name == 'options'
--
--    def test_rules_for_profile_nosuchprofile(self, rule_provider):
--        with pytest.raises(errors.NotFound):
--            rule_provider.rules_for_profile('nosuchprofile')
--
--
--class test_CSRGenerator(object):
--    def test_userCert_OpenSSL(self, generator):
--        principal = {
--            'uid': ['testuser'],
--            'mail': ['testuser@example.com'],
--        }
--        config = {
--            'ipacertificatesubjectbase': [
--                'O=DOMAIN.EXAMPLE.COM'
--            ],
--        }
--
--        script = generator.csr_config(principal, config, 'userCert')
--        with open(os.path.join(
--                CSR_DATA_DIR, 'configs', 'userCert.conf')) as f:
--            expected_script = f.read()
--        assert script == expected_script
--
--    def test_caIPAserviceCert_OpenSSL(self, generator):
--        principal = {
--            'krbprincipalname': [
--                'HTTP/machine.example.com@DOMAIN.EXAMPLE.COM'
--            ],
--        }
--        config = {
--            'ipacertificatesubjectbase': [
--                'O=DOMAIN.EXAMPLE.COM'
--            ],
--        }
--
--        script = generator.csr_config(
--            principal, config, 'caIPAserviceCert')
--        with open(os.path.join(
--                CSR_DATA_DIR, 'configs', 'caIPAserviceCert.conf')) as f:
--            expected_script = f.read()
--        assert script == expected_script
--
--    def test_works_with_lowercase_attr_type_shortname(self, generator):
--        principal = {
--            'uid': ['testuser'],
--            'mail': ['testuser@example.com'],
--        }
--        template_env = {
--            'ipacertificatesubjectbase': [
--                'o=DOMAIN.EXAMPLE.COM'  # lower-case attr type shortname
--            ],
--        }
--        config = generator.csr_config(principal, template_env, 'userCert')
--
--        key = rsa.generate_private_key(
--            public_exponent=65537,
--            key_size=2048,
--            backend=default_backend(),
--        )
--        adaptor = csrgen.OpenSSLAdaptor(key=key)
--
--        reqinfo = bytes(csrgen_ffi.build_requestinfo(
--            config.encode('utf-8'), adaptor.get_subject_public_key_info()))
--        csr_der = adaptor.sign_csr(reqinfo)
--        csr = x509.load_der_x509_csr(csr_der, default_backend())
--        assert (
--            csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
--            == [x509.NameAttribute(x509.NameOID.COMMON_NAME, u'testuser')]
--        )
--        assert (
--            csr.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATION_NAME)
--            == [x509.NameAttribute(
--                x509.NameOID.ORGANIZATION_NAME, u'DOMAIN.EXAMPLE.COM')]
--        )
--
--    def test_unrecognised_attr_type_raises(self, generator):
--        principal = {
--            'uid': ['testuser'],
--            'mail': ['testuser@example.com'],
--        }
--        template_env = {
--            'ipacertificatesubjectbase': [
--                'X=DOMAIN.EXAMPLE.COM'  # unrecognised attr type
--            ],
--        }
--        config = generator.csr_config(principal, template_env, 'userCert')
--
--        key = rsa.generate_private_key(
--            public_exponent=65537,
--            key_size=2048,
--            backend=default_backend(),
--        )
--        adaptor = csrgen.OpenSSLAdaptor(key=key)
--
--        with pytest.raises(
--                errors.CSRTemplateError,
--                message='unrecognised attribute type: X'):
--            csrgen_ffi.build_requestinfo(
--                config.encode('utf-8'), adaptor.get_subject_public_key_info())
--
--
--class test_rule_handling(object):
--    def test_optionalAttributeMissing(self, generator):
--        principal = {'uid': 'testuser'}
--        rule_provider = StubRuleProvider()
--        rule_provider.data_rule.template = '{{subject.mail}}'
--        rule_provider.data_rule.options = {'data_source': 'subject.mail'}
--        generator = csrgen.CSRGenerator(
--            rule_provider, formatter_class=IdentityFormatter)
--
--        script = generator.csr_config(
--            principal, {}, 'example')
--        assert script == '\n'
--
--    def test_twoDataRulesOneMissing(self, generator):
--        principal = {'uid': 'testuser'}
--        rule_provider = StubRuleProvider()
--        rule_provider.data_rule.template = '{{subject.mail}}'
--        rule_provider.data_rule.options = {'data_source': 'subject.mail'}
--        rule_provider.field_mapping.data_rules.append(csrgen.Rule(
--            'data2', '{{subject.uid}}', {'data_source': 'subject.uid'}))
--        generator = csrgen.CSRGenerator(
--            rule_provider, formatter_class=IdentityFormatter)
--
--        script = generator.csr_config(principal, {}, 'example')
--        assert script == ',testuser\n'
--
--    def test_requiredAttributeMissing(self):
--        principal = {'uid': 'testuser'}
--        rule_provider = StubRuleProvider()
--        rule_provider.data_rule.template = '{{subject.mail}}'
--        rule_provider.data_rule.options = {'data_source': 'subject.mail'}
--        rule_provider.syntax_rule.options = {'required': True}
--        generator = csrgen.CSRGenerator(
--            rule_provider, formatter_class=IdentityFormatter)
--
--        with pytest.raises(errors.CSRTemplateError):
--            _script = generator.csr_config(
--                principal, {}, 'example')
--- 
-2.13.6
-
diff --git a/SOURCES/freeipa-4.7.1.tar.gz.asc b/SOURCES/freeipa-4.7.1.tar.gz.asc
deleted file mode 100644
index 3f461b5..0000000
--- a/SOURCES/freeipa-4.7.1.tar.gz.asc
+++ /dev/null
@@ -1,16 +0,0 @@
------BEGIN PGP SIGNATURE-----
-
-iQIzBAABCAAdFiEEL88tWdi+3Yi1YO6/QPd0nE8v3u0FAlu3sM0ACgkQQPd0nE8v
-3u3frhAAgZMF8ifQwBFMs1bQV6AhgP7RCnPxh2qDmcVccbCx3ug655wvawzXZw5H
-171ec7mDnoSD2VtJv2PZ4L3migO+JOlEQg7Yw50DihLnXKrH70hqByvSqOOacsPX
-hdrXRmOMGrG6l/qXs9OF2PIvXVGtIgRKcsQFQ8jyBFvRKFlxY/NzWUG3RmT8IRZm
-LWfqS66If6n99QvR8zCQTARvTUC5hsgS1W00eLEMcAlTs8SYWf3NiQlov1Eucj0L
-wDsvDKoKChXYoQqF4+ymCwPJ/MWvBxtYGwXo9wKVa0itC/Y+HdrCR6QVeL5bQalf
-9bLFIYMuBwt7jPWaz5iST/sl6Svh7pDrIGod7I/G0bhzFwqFxFvINVY5WUGRAske
-ElfvN2RMqjRwrd63AZWXs9kfsTYSZ1oPb7OrMBuAGkoXcfbHQAQesXou9gIGi3yJ
-QGX6neghtop8fKDpNH1iLamVzlM2DZl5po1kiA5/ltbjOiXL86UORRia8CXew6rz
-B9QKEhNorWb6jakXlBUmbDVbew29ZQ9wfOAlVOwF2No7Wr51EC+kXUuLkliBgCfM
-QWhdBJ9Ae8eFGQAgRpd2DPzhMU68N+DplIGyJQfBs+3qDK7Ods+eTmi/UAuXdiYG
-qUE3Gzfa4LfXedlTX3Qcyo48CPVZPNCLh2qcGKtWXuN/pSEOkM0=
-=esuk
------END PGP SIGNATURE-----
diff --git a/SOURCES/freeipa-4.8.0.tar.gz.asc b/SOURCES/freeipa-4.8.0.tar.gz.asc
new file mode 100644
index 0000000..06b7f8e
--- /dev/null
+++ b/SOURCES/freeipa-4.8.0.tar.gz.asc
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCgAdFiEEhAodHH8+xLL+UwQ1RxniuKu/YhoFAl0cTrUACgkQRxniuKu/
+YhoehQ//YuCG+Rq2wbkSDiooP8/0K5HvO/atz4Ke8iaKsOKS8gdmaJTyMnsOOoTc
+hlx925TIOc4/Xp1+qeIn7T8xZp3rYtGcMcxIyKlUrHCU1Jxc07zf+ZlSwCZTCjLU
+YGAh9ReC9+//1oJqnr/C+Rp3veZbYn3DIG261GMqOlHCUfF91hF78XctzklcZNpV
+D38a+gfXdWivejezA/GWyiY3foIcLI98zpBd2v0PXEzaKO2BqrVlOl2nDC7BGapS
+PvpB4GPwuwo8qxASFbu8I6uxyp2oDZtrM/Tb1HM31cuslieH5p/XRwJ8zoewHvgo
+jSKXfcBHmRvjMjSL07R3b7JjZ+1jmj/C5VNXQcPfp2qdhDhmywDArfC3uIBJ2otx
+oxKbtAhAzeGIaoyfgjrxk0ZOubnIbmk/M8nan2F9ChJV/NoKVjDVAfUDDM2h6wXg
+IRBg6uIOkJAKuOr7i0zaxBkBi/8NpUE214JvJnNfWa1gpoYu1S5tzuja6dSeteRM
+JTPPzpkKD2sgK/laRmZQo2si1qFOGEYnLTO0dWJV4/ScZCCy9+rCQ0C6NwKYC7xy
+8c1Juu/YqJF/14VbYAWQIABIK/0z0TiVI0r0v75rzSFpMiThrgC6wXo1zFEADiK0
+GSoYwkcygn0ne21jQxLizGAPRYvdQ5RkpiE2/J4nVSTyuSM/cM4=
+=Q29c
+-----END PGP SIGNATURE-----
diff --git a/SPECS/ipa.spec b/SPECS/ipa.spec
index d5238cb..28e739d 100644
--- a/SPECS/ipa.spec
+++ b/SPECS/ipa.spec
@@ -1,9 +1,12 @@
 # 389-ds-base 1.4 no longer supports i686 platform, build only client
 # packages, https://bugzilla.redhat.com/show_bug.cgi?id=1544386
-%ifarch %{ix86}
-%{!?ONLY_CLIENT:%global ONLY_CLIENT 1}
+%if 0%{?fedora} >= 28 || 0%{?rhel} > 7
+    %ifarch %{ix86}
+        %{!?ONLY_CLIENT:%global ONLY_CLIENT 1}
+    %endif
 %endif
 
+
 # Define ONLY_CLIENT to only make the ipa-client and ipa-python
 # subpackages
 %{!?ONLY_CLIENT:%global ONLY_CLIENT 0}
@@ -13,9 +16,13 @@
     %global enable_server_option --enable-server
 %endif
 
-# Build with ipatests
-# RHEL spec file only: Force ipatests off
-%global with_ipatests 0
+# Build ipatests
+%if 0%{?rhel}
+    %global with_ipatests 0
+%endif
+%if ! %{ONLY_CLIENT}
+    %{!?with_ipatests:%global with_ipatests 1}
+%endif
 %if 0%{?with_ipatests}
     %global with_ipatests_option --with-ipatests
 %else
@@ -23,25 +30,19 @@
 %endif
 
 # Python 2/3 packages and default Python interpreter
-%if 0%{?rhel}
-%global with_default_python 3
-%global with_python2 0
+%if 0%{?rhel} > 7
+    %global with_default_python 3
 %endif
 
 %if 0%{?fedora} >= 29
-# F29 only supports Python 3 as default Python
-%global with_default_python 3
+    # F29 only supports Python 3 as default Python
+    %global with_default_python 3
 %endif
 
 %{!?with_default_python:%global with_default_python 3}
-%{!?with_python2:%global with_python2 1}
 
-%if %{with_default_python} == 3
+%global with_python3 1
 %global python %{__python3}
-%else
-%global with_python2 1
-%global python %{__python2}
-%endif
 
 # lint is not executed during rpmbuild
 # %%global with_lint 1
@@ -51,11 +52,10 @@
     %global linter_options --disable-pylint --without-jslint
 %endif
 
-# RHEL spec file only: alt name is freeipa
-
-%global alt_name freeipa
 %if 0%{?rhel}
-%global krb5_version 1.16.1
+%global package_name ipa
+%global alt_name freeipa
+%global krb5_version 1.17-7
 %global krb5_kdb_version 7.0
 # 0.7.16: https://github.com/drkjam/netaddr/issues/71
 %global python_netaddr_version 0.7.16
@@ -64,66 +64,85 @@
 %global selinux_policy_version 3.14.1-14
 %global slapi_nis_version 0.56.1-4
 %global python_ldap_version 3.1.0-1
-%global ds_version 1.4.0.8-1
-%global hostname_version 3.20-6
+# python3-lib389
+# Fix for "Installation fails: Replica Busy"
+# https://pagure.io/389-ds-base/issue/49818
+%global ds_version 1.4.0.16
+
 %else
-%global krb5_version 1.16.1
+# Fedora
+%global package_name freeipa
+%global alt_name ipa
+# Fix for CVE-2018-20217
+%global krb5_version 1.17-17
 %global krb5_kdb_version 7.0
 # 0.7.16: https://github.com/drkjam/netaddr/issues/71
 %global python_netaddr_version 0.7.16
 # Require 4.7.0 which brings Python 3 bindings
-%global samba_version 2:4.7.0
+%global samba_version 2:4.9.0
 # DNSSEC AVC violation, RHBZ#1537971
 %global selinux_policy_version 3.13.1-283.24
 %global slapi_nis_version 0.56.1
 
 # fix for segfault in python3-ldap, https://pagure.io/freeipa/issue/7324
 %global python_ldap_version 3.1.0-1
+# Fix for create suffix
+# https://pagure.io/389-ds-base/issue/49984
+%global ds_version 1.4.1.1
 
-# Fix for "Crash when failing to read from SASL connection"
-# https://pagure.io/389-ds-base/issue/49639
-%global ds_version 1.4.0.8-1
+# Don't use Fedora's Python dependency generator on Fedora 30/rawhide yet.
+# Some packages don't provide new dist aliases.
+# https://docs.fedoraproject.org/en-US/packaging-guidelines/Python/
+%{?python_disable_dependency_generator}
 
-%global hostname_version 3.20-5
 %endif  # Fedora
 
-# Require Dogtag PKI 10.6.6 to detect when fips is available,
-# https://pagure.io/freeipa/issue/7608
-# Require Dogtag PKI 10.6.7 due to change of handling for the default token name
-# https://pagure.io/dogtagpki/issue/3073
-# and lack of creating admin NSS db twice
-# https://pagure.io/freeipa/issue/7742
-%global pki_version 10.6.7
- 
-# NSS release with fix for CKA_LABEL import bug in shared SQL database.
-# https://bugzilla.redhat.com/show_bug.cgi?id=1568271
-%global nss_version 3.36.1-1.1
+# Require Dogtag PKI 10.6.8-3 (10.6.7 was never pushed to stable)
+# 10.6.7 fixes UpdateNumberRange clone installation issue
+# https://pagure.io/freeipa/issue/7654 and empty token issue
+# and https://pagure.io/dogtagpki/issue/3073
+%global pki_version 10.7.1-2
+
+# https://pagure.io/certmonger/issue/90
+%global certmonger_version 0.79.7-3
+
+# NSS release with fix for p11-kit-proxy issue, affects F28
+# https://pagure.io/freeipa/issue/7810
+%if 0%{?fedora} == 28
+%global nss_version 3.41.0-3
+%else
+%global nss_version 3.41.0-1
+%endif
 
-# One-Way Trust authenticated by trust secret
-# https://bugzilla.redhat.com/show_bug.cgi?id=1345975#c20
-%global sssd_version 2.0.0
+%global sssd_version 2.2.0-1
 
-%define krb5_base_version %(LC_ALL=C rpm -q --qf '%%{VERSION}' krb5-devel | grep -Eo '^[^.]+\.[^.]+')
+%global kdcproxy_version 0.4-3
 
 %global plugin_dir %{_libdir}/dirsrv/plugins
 %global etc_systemd_dir %{_sysconfdir}/systemd/system
 %global gettext_domain ipa
 
-%global VERSION 4.7.1
-
 %define _hardened_build 1
 
-# RHEL spec file only: name is "ipa"
-Name:           ipa
-Version:        %{VERSION}
-Release:        11%{?dist}
+# Work-around fact that RPM SPEC parser does not accept
+# "Version: @VERSION@" in freeipa.spec.in used for Autoconf string replacement
+%define IPA_VERSION 4.8.0
+%define AT_SIGN @
+# redefine IPA_VERSION only if its value matches the Autoconf placeholder
+%if "%{IPA_VERSION}" == "%{AT_SIGN}VERSION%{AT_SIGN}"
+     %define IPA_VERSION nonsense.to.please.RPM.SPEC.parser
+%endif
+
+Name:           %{package_name}
+Version:        %{IPA_VERSION}
+Release:        10%{?dist}
 Summary:        The Identity, Policy and Audit system
 
-Group:          System Environment/Base
 License:        GPLv3+
-URL:            https://www.freeipa.org/
-Source0:        https://releases.pagure.org/freeipa/freeipa-%{VERSION}.tar.gz
-Source1:        https://releases.pagure.org/freeipa/freeipa-%{VERSION}.tar.gz.asc
+URL:            http://www.freeipa.org/
+Source0:        https://releases.pagure.org/freeipa/freeipa-%{version}.tar.gz
+Source1:        https://releases.pagure.org/freeipa/freeipa-%{version}.tar.gz.asc
+
 # RHEL spec file only: START: Change branding to IPA and Identity Management
 # Moved branding logos and background to redhat-logos-ipa-80.4:
 # header-logo.png, login-screen-background.jpg, login-screen-logo.png,
@@ -132,40 +151,17 @@ Source1:        https://releases.pagure.org/freeipa/freeipa-%{VERSION}.tar.gz.as
 
 # RHEL spec file only: START
 Patch0001:      0001-No-need-to-call-rhel-specific-domainname-service.patch
-Patch0002:      0002-freeipa-4.7.0-ipaclient-Remove-no-sssd-and-noac-options_rhbz#1614301.patch
-Patch0003:      0003-adtrust-define-Guests-mapping.patch
-Patch0004:      0004-freeipa-4.7.1-Find_orphan_automember_rules_rhbz#1638373.patch
-Patch0005:      0005-net-groupmap-force-using-empty-config.patch
-Patch0006:      0006-Keep-dogtags-client-db-in-external-ca-step-1.patch
-Patch0007:      0007-Replace-hard-coded-interpreter-with-sys.executable.patch
-Patch0008:      0008-Fix_misleading_errors_during_client_install_rollback_rhbz#1658283.patch
-Patch0009:      0009-ipa-advise_update_url_of_cacerdir_rehash_tool_9cfd07e_rhbz#1658287.patch
-Patch0010:      0010-Handle_NTP_configuration_in_replica_server_installation_f3e3da5_rhbz#1651679.patch
-Patch0011:      0011-Fix_defects_found_by_static_analysis_rhbz#1658182.patch
-Patch0012:      0012-ipa-replica-install_--setup-adtrust_check_for_package_ipa-server-trust-ad_rhbz#1658294.patch
-Patch0013:      0013-ipaldap_invalid_modlist_when_attribute_encoding_can_vary_rhbz#1658302.patch
-Patch0014:      0014-Allow_ipaapi_and_Apache_user_to_access_SSSD_IFP_rhbz#1639910.patch
-Patch0015:      0015-Add_sysadm_r_to_default_SELinux_user_map_order_1853e2e_rhbz#1658303.patch
-Patch0016:      0016-certdb_non-empty_Subject_Key_Identifier_and_validate_server_cert_sig_rhbz#1641988.patch
-Patch0017:      0017-ipa-replica-install_password_and_admin-password_options_mutually_exclusive_rhbz#1658309.patch
-Patch0018:      0018-ipa_upgrade_handle_double-encoded_certificates_rhbz#1658310.patch
-Patch0019:      0019-PKINIT_fix_ipa-pkinit-manage_enable_disable_rhbz#1658313.patch
-Patch0020:      0020-Enable_LDAP_debug_output_in_client_to_display_TLS_errors_in_join_rhbz#1658316.patch
-Patch0021:      0021-rpc_always_read_response_rhbz#1639890.patch
-Patch0022:      0022-ipa_vault-retrieve_fix_internal_error_rhbz#1658485.patch
-Patch0023:      0023-Move_ipa_s_systemd_tmpfiles_from_var_run_to_run_rhbz#1658487.patch
-Patch0024:      0024-Fix_authselect_invocations_to_work_with_1.0.2_rhbz#1654291.patch
-Patch0025:      0025-ipa-client-automount_and_NFS_unit_name_changes_rhbz#1645501.patch
-Patch0026:      0026-Fix_compile_issue_with_new_389-ds_rhbz#1659448.patch
-Patch0027:      0027-ipaserver-dcerpc-fix-exclusion-entry-with-a-forest-t.patch
-Patch0028:      0028-Create-systemd-user-HBAC-service-and-rule.patch
-# Patch0029 contains small excerpt from upstream 8182ebc to have user_add in
-# ipatests/pytest_ipa/integration/tasks.py to be able to have working test
-Patch0029:      0029-Resolve_user_group_names_in_idoverride_-find_rhbz#1657745.patch
-Patch0030:      0030-Fix-systemd-user-HBAC-rule.patch
-Patch0031:      0031-ipa-client-automount-handle-NFS-configuration-file-c.patch
+Patch0002:      0002-Fix-test_webui.test_selinuxusermap.patch
+Patch0003:      0003-Remove-posixAccount-from-service_find-search-filter-2f9cbff_rhbz#1731437.patch
+Patch0004:      0004-Repeated-uninstallation-of-ipa-client-samba-crashes_rhbz#1732529.patch
+Patch0005:      0005-WebUI-Add-PKINIT-status-field-to-Configuration-page-a46383f_rhbz#1518153.patch
+Patch0006:      0006-external-ca-profile-fix_rhbz#1731813.patch
+Patch0007:      0007-Allow-insecure-binds-for-migration-8e207fd3_rhbz#1731963.patch
+Patch0008:      0008-install-Add-missing-scripts-to-app_DATA_rhbz#1741170.patch
+Patch0009:      0009-extdom-unify-error-code-handling-especially-LDAP_NO_SUCH_OBJECT_rhbz#1741530.patch
+Patch0010:      0010-Fix-automount-behavior-with-authselect_rhbz#1740167.patch
 Patch1001:      1001-Change-branding-to-IPA-and-Identity-Management.patch
-Patch1002:      1002-Remove-csrgen.patch
+Patch1002:      1002-4.8.0-Remove-csrgen.patch
 # RHEL spec file only: END
 
 # For the timestamp trick in patch application
@@ -188,10 +184,6 @@ BuildRequires:  automake
 BuildRequires:  libtool
 BuildRequires:  gettext
 BuildRequires:  gettext-devel
-%if 0%{?with_python2}
-BuildRequires:  python2-devel
-BuildRequires:  python2-setuptools
-%endif # with_python2
 BuildRequires:  python3-devel
 BuildRequires:  python3-setuptools
 BuildRequires:  systemd
@@ -224,7 +216,6 @@ BuildRequires:  python3-lesscpy >= 0.13.0-2
 #
 # Build dependencies for makeapi/makeaci
 #
-%if %{with_default_python} == 3
 BuildRequires:  python3-cffi
 BuildRequires:  python3-dns
 BuildRequires:  python3-ldap >= %{python_ldap_version}
@@ -233,16 +224,6 @@ BuildRequires:  python3-netaddr >= %{python_netaddr_version}
 BuildRequires:  python3-pyasn1
 BuildRequires:  python3-pyasn1-modules
 BuildRequires:  python3-six
-%else
-BuildRequires:  python2-cffi
-BuildRequires:  python2-dns
-BuildRequires:  python2-ldap >= %{python_ldap_version}
-BuildRequires:  python2-libsss_nss_idmap
-BuildRequires:  python2-netaddr >= %{python_netaddr_version}
-BuildRequires:  python2-pyasn1
-BuildRequires:  python2-pyasn1-modules
-BuildRequires:  python2-six
-%endif
 
 #
 # Build dependencies for wheel packaging and PyPI upload
@@ -251,7 +232,11 @@ BuildRequires:  python2-six
 BuildRequires:  dbus-glib-devel
 BuildRequires:  libffi-devel
 BuildRequires:  python3-tox
+%if 0%{?fedora} <= 28
 BuildRequires:  python3-twine
+%else
+BuildRequires:  twine
+%endif
 BuildRequires:  python3-wheel
 %endif # with_wheels
 
@@ -260,49 +245,9 @@ BuildRequires:  python3-wheel
 #
 %if 0%{?with_lint}
 BuildRequires:  jsl
+BuildRequires:  rpmlint
 BuildRequires:  softhsm
 
-%if 0%{?with_python2}
-BuildRequires:  python2-augeas
-BuildRequires:  python2-cffi
-BuildRequires:  python2-cryptography >= 1.6
-BuildRequires:  python2-custodia >= 0.3.1
-BuildRequires:  python2-dateutil
-BuildRequires:  python2-dbus
-BuildRequires:  python2-dns
-BuildRequires:  python2-dns >= 1.15
-BuildRequires:  python2-enum34
-BuildRequires:  python2-gssapi >= 1.2.0-5
-BuildRequires:  python2-jinja2
-BuildRequires:  python2-jwcrypto >= 0.4.2
-BuildRequires:  python2-ldap >= %{python_ldap_version}
-BuildRequires:  python2-libipa_hbac
-BuildRequires:  python2-libsss_nss_idmap
-BuildRequires:  python2-lxml
-BuildRequires:  python2-netaddr >= %{python_netaddr_version}
-BuildRequires:  python2-netifaces
-BuildRequires:  python2-paste
-%if 0%{?fedora} < 29
-# Fedora 29 workaround: Dogtag no longer packages Python 2
-BuildRequires:  python2-pki >= %{pki_version}
-%endif
-BuildRequires:  python2-polib
-BuildRequires:  python2-pyasn1
-BuildRequires:  python2-pyasn1-modules
-BuildRequires:  python2-pycodestyle
-BuildRequires:  python2-pylint
-BuildRequires:  python2-pytest-multihost
-BuildRequires:  python2-pytest-sourceorder
-BuildRequires:  python2-qrcode-core >= 5.0.0
-BuildRequires:  python2-samba
-BuildRequires:  python2-six
-BuildRequires:  python2-sss
-BuildRequires:  python2-sss-murmur
-BuildRequires:  python2-sssdconfig >= %{sssd_version}
-BuildRequires:  python2-systemd
-BuildRequires:  python2-yubico
-%endif # with_python2
-
 BuildRequires:  python3-augeas
 BuildRequires:  python3-cffi
 BuildRequires:  python3-cryptography >= 1.6
@@ -315,6 +260,7 @@ BuildRequires:  python3-jinja2
 BuildRequires:  python3-jwcrypto >= 0.4.2
 BuildRequires:  python3-ldap >= %{python_ldap_version}
 BuildRequires:  python3-ldap >= %{python_ldap_version}
+BuildRequires:  python3-lib389 >= %{ds_version}
 BuildRequires:  python3-libipa_hbac
 BuildRequires:  python3-libsss_nss_idmap
 BuildRequires:  python3-lxml
@@ -326,7 +272,12 @@ BuildRequires:  python3-polib
 BuildRequires:  python3-pyasn1
 BuildRequires:  python3-pyasn1-modules
 BuildRequires:  python3-pycodestyle
+%if 0%{?fedora} >= 29
+# https://bugzilla.redhat.com/show_bug.cgi?id=1648299
+BuildRequires:  python3-pylint >= 2.1.1-2
+%else
 BuildRequires:  python3-pylint >= 1.7
+%endif
 BuildRequires:  python3-pytest-multihost
 BuildRequires:  python3-pytest-sourceorder
 BuildRequires:  python3-qrcode-core >= 5.0.0
@@ -360,41 +311,26 @@ and integration with Active Directory based infrastructures (Trusts).
 
 %package server
 Summary: The IPA authentication server
-Group: System Environment/Base
 Requires: %{name}-server-common = %{version}-%{release}
 Requires: %{name}-client = %{version}-%{release}
 Requires: %{name}-common = %{version}-%{release}
-%if %{with_default_python} == 3
 Requires: python3-ipaserver = %{version}-%{release}
 Requires: python3-ldap >= %{python_ldap_version}
-%else
-Requires: python2-ipaserver = %{version}-%{release}
-Requires: python2-ldap >= %{python_ldap_version}
-%endif
 Requires: 389-ds-base >= %{ds_version}
-Requires: 389-ds-base-legacy-tools >= %{ds_version}
 Requires: openldap-clients > 2.4.35-4
 Requires: nss >= %{nss_version}
 Requires: nss-tools >= %{nss_version}
 Requires(post): krb5-server >= %{krb5_version}
-Requires(post): krb5-server >= %{krb5_base_version}, krb5-server < %{krb5_base_version}.100
+Requires(post): krb5-kdb-version = %{krb5_kdb_version}
 Requires: krb5-pkinit-openssl >= %{krb5_version}
 Requires: cyrus-sasl-gssapi%{?_isa}
 Requires: chrony
 Requires: httpd >= 2.4.6-31
-%if %{with_default_python} == 3
-%{?__python3:Requires(preun): %{__python3}}
-%{?__python3:Requires(postun): %{__python3}}
+Requires(preun): python3
+Requires(postun): python3
 Requires: python3-gssapi >= 1.2.0-5
 Requires: python3-systemd
 Requires: python3-mod_wsgi
-%else
-Requires(preun): python2
-Requires(postun): python2
-Requires: python2-gssapi >= 1.2.0-5
-Requires: python2-systemd
-Requires: mod_wsgi
-%endif
 Requires: mod_auth_gssapi >= 1.5.0
 Requires: mod_ssl
 Requires: mod_session
@@ -408,17 +344,13 @@ Requires(post): systemd-units
 Requires: selinux-policy >= %{selinux_policy_version}
 Requires(post): selinux-policy-base >= %{selinux_policy_version}
 Requires: slapi-nis >= %{slapi_nis_version}
-# jss is an indirect dependency. 4.4.5 fixes sub CA replication bug,
-# see https://pagure.io/freeipa/issue/7536
-# see https://pagure.io/freeipa/issue/7590
-Requires: jss >= 4.4.5-1
 Requires: pki-ca >= %{pki_version}
 Requires: pki-kra >= %{pki_version}
 Requires(preun): systemd-units
 Requires(postun): systemd-units
 Requires: policycoreutils >= 2.1.12-5
 Requires: tar
-Requires(pre): certmonger >= 0.79.5-1
+Requires(pre): certmonger >= %{certmonger_version}
 Requires(pre): 389-ds-base >= %{ds_version}
 Requires: fontawesome-fonts
 Requires: open-sans-fonts
@@ -430,7 +362,6 @@ Requires: gzip
 Requires: oddjob
 # 0.7.0-2: https://pagure.io/gssproxy/pull-request/172
 Requires: gssproxy >= 0.7.0-2
-# 1.15.2: FindByNameAndCertificate (https://pagure.io/SSSD/sssd/issue/3050)
 Requires: sssd-dbus >= %{sssd_version}
 
 Provides: %{alt_name}-server = %{version}
@@ -462,51 +393,12 @@ and integration with Active Directory based infrastructures (Trusts).
 If you are installing an IPA server, you need to install this package.
 
 
-%if 0%{?with_python2} && 0%{?fedora} < 29
-# Fedora 29 workaround: don't build python2-ipaserver, python2-pki is n/a
-%package -n python2-ipaserver
-Summary: Python libraries used by IPA server
-Group: System Environment/Libraries
-BuildArch: noarch
-%{?python_provide:%python_provide python2-ipaserver}
-%{!?python_provide:Provides: python-ipaserver = %{version}-%{release}}
-Requires: %{name}-server-common = %{version}-%{release}
-Requires: %{name}-common = %{version}-%{release}
-Requires: python2-augeas
-Requires: python2-custodia >= 0.3.1
-Requires: python2-dbus
-Requires: python2-dns >= 1.15
-Requires: python2-gssapi >= 1.2.0-5
-Requires: python2-ipaclient = %{version}-%{release}
-Requires: python2-kdcproxy >= 0.3
-Requires: python2-ldap >= %{python_ldap_version}
-Requires: python2-lxml
-Requires: python2-pki >= %{pki_version}
-Requires: python2-pyasn1 >= 0.3.2-2
-Requires: python2-sssdconfig >= %{sssd_version}
-Requires: rpm-libs
-
-%description -n python2-ipaserver
-IPA is an integrated solution to provide centrally managed Identity (users,
-hosts, services), Authentication (SSO, 2FA), and Authorization
-(host access control, SELinux user roles, services). The solution provides
-features for further integration with Linux based clients (SUDO, automount)
-and integration with Active Directory based infrastructures (Trusts).
-If you are installing an IPA server, you need to install this package.
-
-%endif  # with_python2 and Fedora < 29
-
 %package -n python3-ipaserver
 Summary: Python libraries used by IPA server
-Group: System Environment/Libraries
 BuildArch: noarch
 %{?python_provide:%python_provide python3-ipaserver}
 Requires: %{name}-server-common = %{version}-%{release}
 Requires: %{name}-common = %{version}-%{release}
-%if 0%{?fedora} >= 29
-Conflicts: python2-ipaserver
-Obsoletes: python2-ipaserver < %{version}
-%endif
 # we need pre-requires since earlier versions may break upgrade
 Requires(pre): python3-ldap >= %{python_ldap_version}
 Requires: python3-augeas
@@ -515,7 +407,7 @@ Requires: python3-dbus
 Requires: python3-dns >= 1.15
 Requires: python3-gssapi >= 1.2.0
 Requires: python3-ipaclient = %{version}-%{release}
-Requires: python3-kdcproxy >= 0.3
+Requires: python3-kdcproxy >= %{kdcproxy_version}
 Requires: python3-lxml
 Requires: python3-pki >= %{pki_version}
 Requires: python3-pyasn1 >= 0.3.2-2
@@ -533,13 +425,14 @@ If you are installing an IPA server, you need to install this package.
 
 %package server-common
 Summary: Common files used by IPA server
-Group: System Environment/Base
 BuildArch: noarch
 Requires: %{name}-client-common = %{version}-%{release}
 Requires: httpd >= 2.4.6-31
 Requires: systemd-units >= 38
 Requires: custodia >= 0.3.1
+%if 0%{?rhel} > 7
 Requires: redhat-logos-ipa >= 80.4
+%endif
 
 Provides: %{alt_name}-server-common = %{version}
 Conflicts: %{alt_name}-server-common
@@ -556,7 +449,6 @@ If you are installing an IPA server, you need to install this package.
 
 %package server-dns
 Summary: IPA integrated DNS server with support for automatic DNSSEC signing
-Group: System Environment/Base
 BuildArch: noarch
 Requires: %{name}-server = %{version}-%{release}
 Requires: bind-dyndb-ldap >= 11.0-2
@@ -581,7 +473,6 @@ Integrated DNS server is BIND 9. OpenDNSSEC provides key management.
 
 %package server-trust-ad
 Summary: Virtual package to install packages required for Active Directory trusts
-Group: System Environment/Base
 Requires: %{name}-server = %{version}-%{release}
 Requires: %{name}-common = %{version}-%{release}
 
@@ -589,17 +480,10 @@ Requires: samba >= %{samba_version}
 Requires: samba-winbind
 Requires: libsss_idmap
 
-%if %{with_default_python} == 3
-%{?__python3:Requires(post): %{__python3}}
+Requires(post): python3
 Requires: python3-samba
 Requires: python3-libsss_nss_idmap
 Requires: python3-sss
-%else
-Requires(post): python2
-Requires: python2-samba
-Requires: python2-libsss_nss_idmap
-Requires: python2-sss
-%endif  # with_default_python
 
 # We use alternatives to divert winbind_krb5_locator.so plugin to libkrb5
 # on the installes where server-trust-ad subpackage is installed because
@@ -623,35 +507,23 @@ dependencies at once.
 
 %package client
 Summary: IPA authentication for use on clients
-Group: System Environment/Base
 Requires: %{name}-client-common = %{version}-%{release}
 Requires: %{name}-common = %{version}-%{release}
-%if %{with_default_python} == 3
 Requires: python3-gssapi >= 1.2.0-5
 Requires: python3-ipaclient = %{version}-%{release}
 Requires: python3-ldap >= %{python_ldap_version}
 Requires: python3-sssdconfig >= %{sssd_version}
-%else
-Requires: python2-gssapi >= 1.2.0-5
-Requires: python2-ipaclient = %{version}-%{release}
-Requires: python2-ldap >= %{python_ldap_version}
-Requires: python2-sssdconfig >= %{sssd_version}
-%endif
 Requires: cyrus-sasl-gssapi%{?_isa}
 Requires: chrony
 Requires: krb5-workstation >= %{krb5_version}
 Requires: authselect >= 0.4-2
 Requires: curl
 # NIS domain name config: /usr/lib/systemd/system/*-domainname.service
-%if 0%{?fedora} >= 29 || 0%{?rhel}
-Requires: hostname >= %{hostname_version}
-%else
-Requires: initscripts
-%endif
+Requires: hostname
 Requires: libcurl >= 7.21.7-2
 Requires: xmlrpc-c >= 1.27.4
 Requires: sssd-ipa >= %{sssd_version}
-Requires: certmonger >= 0.79.5-1
+Requires: certmonger >= %{certmonger_version}
 Requires: nss-tools >= %{nss_version}
 Requires: bind-utils
 Requires: oddjob-mkhomedir
@@ -683,35 +555,25 @@ If your network uses IPA for authentication, this package should be
 installed on every client machine.
 This package provides command-line tools for IPA administrators.
 
-%if 0%{?with_python2}
-
-%package -n python2-ipaclient
-Summary: Python libraries used by IPA client
-Group: System Environment/Libraries
-BuildArch: noarch
-%{?python_provide:%python_provide python2-ipaclient}
-%{!?python_provide:Provides: python-ipaclient = %{version}-%{release}}
-Requires: %{name}-client-common = %{version}-%{release}
-Requires: %{name}-common = %{version}-%{release}
-Requires: python2-ipalib = %{version}-%{release}
-Requires: python2-augeas
-Requires: python2-dns >= 1.15
-Requires: python2-jinja2
-
-%description -n python2-ipaclient
-IPA is an integrated solution to provide centrally managed Identity (users,
-hosts, services), Authentication (SSO, 2FA), and Authorization
-(host access control, SELinux user roles, services). The solution provides
-features for further integration with Linux based clients (SUDO, automount)
-and integration with Active Directory based infrastructures (Trusts).
-If your network uses IPA for authentication, this package should be
-installed on every client machine.
+%package client-samba
+Summary: Tools to configure Samba on IPA client
+Group: System Environment/Base
+Requires: %{name}-client = %{version}-%{release}
+Requires: python3-samba
+Requires: samba-client
+Requires: samba-winbind
+Requires: samba-common-tools
+Requires: samba
+Requires: sssd-winbind-idmap
+Requires: tdb-tools
+Requires: cifs-utils
 
-%endif  # with_python2
+%description client-samba
+This package provides command-line tools to deploy Samba domain member
+on the machine enrolled into a FreeIPA environment
 
 %package -n python3-ipaclient
 Summary: Python libraries used by IPA client
-Group: System Environment/Libraries
 BuildArch: noarch
 %{?python_provide:%python_provide python3-ipaclient}
 Requires: %{name}-client-common = %{version}-%{release}
@@ -719,6 +581,7 @@ Requires: %{name}-common = %{version}-%{release}
 Requires: python3-ipalib = %{version}-%{release}
 Requires: python3-augeas
 Requires: python3-dns >= 1.15
+Requires: python3-jinja2
 # RHEL spec file only: DELETED: Remove csrgen
 
 %description -n python3-ipaclient
@@ -733,7 +596,6 @@ installed on every client machine.
 
 %package client-common
 Summary: Common files used by IPA client
-Group: System Environment/Base
 BuildArch: noarch
 
 Provides: %{alt_name}-client-common = %{version}
@@ -752,16 +614,11 @@ installed on every client machine.
 
 %package python-compat
 Summary: Compatiblity package for Python libraries used by IPA
-Group: System Environment/Libraries
 BuildArch: noarch
 Obsoletes: %{name}-python < 4.2.91
 Provides: %{name}-python = %{version}-%{release}
 Requires: %{name}-common = %{version}-%{release}
-%if %{with_default_python} == 3
 Requires: python3-ipalib = %{version}-%{release}
-%else
-Requires: python2-ipalib = %{version}-%{release}
-%endif
 
 Provides: %{alt_name}-python-compat = %{version}
 Conflicts: %{alt_name}-python-compat
@@ -777,66 +634,13 @@ hosts, services), Authentication (SSO, 2FA), and Authorization
 features for further integration with Linux based clients (SUDO, automount)
 and integration with Active Directory based infrastructures (Trusts).
 This is a compatibility package to accommodate %{name}-python split into
-python2-ipalib and %{name}-common. Packages still depending on
+python3-ipalib and %{name}-common. Packages still depending on
 %{name}-python should be fixed to depend on python2-ipaclient or
 %{name}-common instead.
 
-%if 0%{?with_python2}
-
-%package -n python2-ipalib
-Summary: Python libraries used by IPA
-Group: System Environment/Libraries
-BuildArch: noarch
-Conflicts: %{name}-python < 4.2.91
-%{?python_provide:%python_provide python2-ipalib}
-%{!?python_provide:Provides: python-ipalib = %{version}-%{release}}
-Provides: python2-ipapython = %{version}-%{release}
-%{?python_provide:%python_provide python2-ipapython}
-%{!?python_provide:Provides: python-ipapython = %{version}-%{release}}
-Provides: python2-ipaplatform = %{version}-%{release}
-%{?python_provide:%python_provide python2-ipaplatform}
-%{!?python_provide:Provides: python-ipaplatform = %{version}-%{release}}
-Requires: %{name}-common = %{version}-%{release}
-Requires: gnupg2
-Requires: keyutils
-Requires: python2 >= 2.7.9
-Requires: python2-cffi
-Requires: python2-cryptography >= 1.6
-Requires: python2-dateutil
-Requires: python2-dbus
-Requires: python2-dns >= 1.15
-Requires: python2-enum34
-Requires: python2-gssapi >= 1.2.0-5
-Requires: python2-jwcrypto >= 0.4.2
-Requires: python2-ldap >= %{python_ldap_version}
-Requires: python2-libipa_hbac
-Requires: python2-netaddr >= %{python_netaddr_version}
-Requires: python2-netifaces >= 0.10.4
-Requires: python2-pyasn1 >= 0.3.2-2
-Requires: python2-pyasn1-modules >= 0.3.2-2
-Requires: python2-pyusb
-Requires: python2-qrcode-core >= 5.0.0
-Requires: python2-requests
-Requires: python2-setuptools
-Requires: python2-six
-Requires: python2-sss-murmur
-Requires: python2-yubico >= 1.2.3
-
-Conflicts: %{alt_name}-python < %{version}
-
-%description -n python2-ipalib
-IPA is an integrated solution to provide centrally managed Identity (users,
-hosts, services), Authentication (SSO, 2FA), and Authorization
-(host access control, SELinux user roles, services). The solution provides
-features for further integration with Linux based clients (SUDO, automount)
-and integration with Active Directory based infrastructures (Trusts).
-If you are using IPA, you need to install this package.
-
-%endif  # with_python2
 
 %package -n python3-ipalib
 Summary: Python3 libraries used by IPA
-Group: System Environment/Libraries
 BuildArch: noarch
 %{?python_provide:%python_provide python3-ipalib}
 Provides: python3-ipapython = %{version}-%{release}
@@ -883,7 +687,6 @@ If you are using IPA with Python 3, you need to install this package.
 
 %package common
 Summary: Common files used by IPA
-Group: System Environment/Libraries
 BuildArch: noarch
 Conflicts: %{name}-python < 4.2.91
 
@@ -904,55 +707,12 @@ If you are using IPA, you need to install this package.
 
 %if 0%{?with_ipatests}
 
-%if 0%{?with_python2} && 0%{?fedora} < 29
-# Fedora 29 workaround: don't build python2-ipaserver, depends on ipaserver
-%package -n python2-ipatests
-Summary: IPA tests and test tools
-BuildArch: noarch
-Obsoletes: %{name}-tests < 4.2.91
-Provides: %{name}-tests = %{version}-%{release}
-%{?python_provide:%python_provide python2-ipatests}
-%{!?python_provide:Provides: python-ipatests = %{version}-%{release}}
-Requires: python2-ipaclient = %{version}-%{release}
-Requires: python2-ipaserver = %{version}-%{release}
-Requires: iptables
-Requires: ldns-utils
-Requires: python2-coverage
-Requires: python2-cryptography >= 1.6
-Requires: python2-mock
-Requires: python2-paste
-Requires: python2-polib
-Requires: python2-pytest >= 2.6
-Requires: python2-pytest-multihost >= 0.5
-Requires: python2-pytest-sourceorder
-Requires: python2-sssdconfig >= %{sssd_version}
-Requires: tar
-Requires: xz
-
-Provides: %{alt_name}-tests = %{version}
-Conflicts: %{alt_name}-tests
-Obsoletes: %{alt_name}-tests < %{version}
-
-%description -n python2-ipatests
-IPA is an integrated solution to provide centrally managed Identity (users,
-hosts, services), Authentication (SSO, 2FA), and Authorization
-(host access control, SELinux user roles, services). The solution provides
-features for further integration with Linux based clients (SUDO, automount)
-and integration with Active Directory based infrastructures (Trusts).
-This package contains tests that verify IPA functionality.
-
-%endif  # with_python2 and Fedora < 29
-
 %package -n python3-ipatests
 Summary: IPA tests and test tools
 BuildArch: noarch
 %{?python_provide:%python_provide python3-ipatests}
 Requires: python3-ipaclient = %{version}-%{release}
 Requires: python3-ipaserver = %{version}-%{release}
-%if 0%{?fedora} >= 29
-Conflicts: python2-ipatests
-Obsoletes: python2-ipatests < %{version}
-%endif
 Requires: iptables
 Requires: ldns-utils
 Requires: python3-coverage
@@ -993,7 +753,9 @@ UpdateTimestamps() {
   done
 }
 
-%setup -n freeipa-%{VERSION} -q
+%setup -n freeipa-%{version} -q
+
+# RHEL spec file only: START
 
 # To allow proper application patches to the stripped po files, strip originals
 pushd po
@@ -1016,25 +778,6 @@ done
 # product-name.png
 # RHEL spec file only: END
 
-# Workaround: We want to build Python things twice. To be sure we do not mess
-# up something, do two separate builds in separate directories.
-# freeipa-$VER-python3 for Python 3 build
-# freeipa-$VER-python2 for Python 2 build
-# freeipa-$VER is a symlink to default Python version
-
-%if %{with_default_python} == 3
-%if 0%{?with_python2}
-cp -r %{_builddir}/freeipa-%{version} %{_builddir}/freeipa-%{version}-python2
-%endif
-mv %{_builddir}/freeipa-%{version} %{_builddir}/freeipa-%{version}-python3
-ln -sr %{_builddir}/freeipa-%{version}-python3 %{_builddir}/freeipa-%{version}
-%else
-# Python 2 default
-cp -r %{_builddir}/freeipa-%{version} %{_builddir}/freeipa-%{version}-python3
-mv %{_builddir}/freeipa-%{version} %{_builddir}/freeipa-%{version}-python2
-ln -sr %{_builddir}/freeipa-%{version}-python2 %{_builddir}/freeipa-%{version}
-%endif
-
 %build
 # RHEL spec file only: START
 autoreconf -i -f
@@ -1043,35 +786,11 @@ autoreconf -i -f
 # PATH is workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1005235
 export PATH=/usr/bin:/usr/sbin:$PATH
 
-
-%if 0%{?with_python2}
-export PYTHON=%{__python2}
-pushd %{_builddir}/freeipa-%{version}-python2
-# Workaround: make sure all shebangs are pointing to Python 2
-# This should be solved properly using setuptools
-# and this hack should be removed.
-find \
-	! -name '*.pyc' -a \
-	! -name '*.pyo' -a \
-	-type f -exec grep -qsm1 '^#!.*\bpython' {} \; \
-	-exec sed -i -e '1 s|^#!.*\bpython[^ ]*|#!%{__python2}|' {} \;
-
-%configure --with-vendor-suffix=-%{release} \
-           %{enable_server_option} \
-           %{with_ipatests_option} \
-           %{linter_options} \
-           --with-ipaplatform=rhel
-popd
-%endif # ! with_python2
-
 export PYTHON=%{__python3}
-pushd %{_builddir}/freeipa-%{version}-python3
 %configure --with-vendor-suffix=-%{release} \
            %{enable_server_option} \
            %{with_ipatests_option} \
-           %{linter_options} \
-           --with-ipaplatform=rhel
-popd
+           %{linter_options}
 
 # run build in default dir
 # -Onone is workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1398405
@@ -1092,67 +811,17 @@ make %{?_smp_mflags} check VERBOSE=yes LIBDIR=%{_libdir}
 # (These are typically configuration files created by IPA installer.)
 # All other artifacts should be created by make install.
 #
-# Exception to this rule are test programs which where want to install
-# Python2/3 versions at the same time so we need to rename them. Yuck.
-
-# Python 3 packages and commands
-pushd %{_builddir}/freeipa-%{version}-python3
-%{__make} python_install DESTDIR=%{?buildroot} INSTALL="%{__install} -p"
-popd
-%if 0%{?with_ipatests}
-mv %{buildroot}%{_bindir}/ipa-run-tests %{buildroot}%{_bindir}/ipa-run-tests-%{python3_version}
-mv %{buildroot}%{_bindir}/ipa-test-config %{buildroot}%{_bindir}/ipa-test-config-%{python3_version}
-mv %{buildroot}%{_bindir}/ipa-test-task %{buildroot}%{_bindir}/ipa-test-task-%{python3_version}
-ln -rs %{buildroot}%{_bindir}/ipa-run-tests-%{python3_version} %{buildroot}%{_bindir}/ipa-run-tests-3
-ln -rs %{buildroot}%{_bindir}/ipa-test-config-%{python3_version} %{buildroot}%{_bindir}/ipa-test-config-3
-ln -rs %{buildroot}%{_bindir}/ipa-test-task-%{python3_version} %{buildroot}%{_bindir}/ipa-test-task-3
-%endif # with_ipatests
 
-# Python 2 packages and commands
-%if 0%{?with_python2}
-pushd %{_builddir}/freeipa-%{version}-python2
 %{__make} python_install DESTDIR=%{?buildroot} INSTALL="%{__install} -p"
-popd
-%if 0%{?with_python2} && 0%{?fedora} < 29
-# Fedora 29 workaround: don't ship ipatests binaries
-mv %{buildroot}%{_bindir}/ipa-run-tests %{buildroot}%{_bindir}/ipa-run-tests-%{python2_version}
-mv %{buildroot}%{_bindir}/ipa-test-config %{buildroot}%{_bindir}/ipa-test-config-%{python2_version}
-mv %{buildroot}%{_bindir}/ipa-test-task %{buildroot}%{_bindir}/ipa-test-task-%{python2_version}
-ln -rs %{buildroot}%{_bindir}/ipa-run-tests-%{python2_version} %{buildroot}%{_bindir}/ipa-run-tests-2
-ln -rs %{buildroot}%{_bindir}/ipa-test-config-%{python2_version} %{buildroot}%{_bindir}/ipa-test-config-2
-ln -rs %{buildroot}%{_bindir}/ipa-test-task-%{python2_version} %{buildroot}%{_bindir}/ipa-test-task-2
-%endif # with_ipatests and Fedora < 29
-%endif # with_python2
 
 # default installation
 # This installs all Python packages twice and overrides the ipa-test
 # commands. We'll fix the command links later with ln --force.
 %make_install
 
-# Decide which Python (2 or 3) should be used as default for tests
-%if 0%{?with_ipatests}
-%if %{with_default_python} == 3
-ln -frs %{buildroot}%{_bindir}/ipa-run-tests-%{python3_version} %{buildroot}%{_bindir}/ipa-run-tests
-ln -frs %{buildroot}%{_bindir}/ipa-test-config-%{python3_version} %{buildroot}%{_bindir}/ipa-test-config
-ln -frs %{buildroot}%{_bindir}/ipa-test-task-%{python3_version} %{buildroot}%{_bindir}/ipa-test-task
-%else
-ln -frs %{buildroot}%{_bindir}/ipa-run-tests-%{python2_version} %{buildroot}%{_bindir}/ipa-run-tests
-ln -frs %{buildroot}%{_bindir}/ipa-test-config-%{python2_version} %{buildroot}%{_bindir}/ipa-test-config
-ln -frs %{buildroot}%{_bindir}/ipa-test-task-%{python2_version} %{buildroot}%{_bindir}/ipa-test-task
-%endif # with_default_python
-%endif # with_ipatests
-
 # remove files which are useful only for make uninstall
 find %{buildroot} -wholename '*/site-packages/*/install_files.txt' -exec rm {} \;
 
-%if 0%{?with_ipatests} && 0%{?with_python2} && 0%{?fedora} >= 29
-# Fedora 29 workaround: Remove Python 2 ipaserver and ipatests
-rm -rf %{buildroot}%{python2_sitelib}/ipaserver
-rm -rf %{buildroot}%{python2_sitelib}/ipaserver-*.egg-info
-rm -rf %{buildroot}%{python2_sitelib}/ipatests
-rm -rf %{buildroot}%{python2_sitelib}/ipatests-*.egg-info
-%endif  # with python2 ipatests and Fedora >= 29
-
 # RHEL spec file only: START
 # Moved branding logos and background to redhat-logos-ipa-80.4:
 # header-logo.png, login-screen-background.jpg, login-screen-logo.png,
@@ -1211,10 +880,6 @@ mkdir -p %{buildroot}%{_sysconfdir}/cron.d
 %endif # ONLY_CLIENT
 
 
-%clean
-rm -rf %{buildroot}
-
-
 %if ! %{ONLY_CLIENT}
 
 %post server
@@ -1419,14 +1084,21 @@ fi
 %{_sbindir}/ipa-cacert-manage
 %{_sbindir}/ipa-winsync-migrate
 %{_sbindir}/ipa-pkinit-manage
+%{_sbindir}/ipa-crlgen-manage
+%{_sbindir}/ipa-cert-fix
 %{_libexecdir}/certmonger/dogtag-ipa-ca-renew-agent-submit
 %{_libexecdir}/certmonger/ipa-server-guard
+%{_libexecdir}/ipa/custodia/ipa-custodia-dmldap
+%{_libexecdir}/ipa/custodia/ipa-custodia-pki-tomcat
+%{_libexecdir}/ipa/custodia/ipa-custodia-pki-tomcat-wrapped
+%{_libexecdir}/ipa/custodia/ipa-custodia-ra-agent
 %dir %{_libexecdir}/ipa
 %{_libexecdir}/ipa/ipa-custodia
 %{_libexecdir}/ipa/ipa-custodia-check
 %{_libexecdir}/ipa/ipa-httpd-kdcproxy
 %{_libexecdir}/ipa/ipa-httpd-pwdreader
 %{_libexecdir}/ipa/ipa-pki-retrieve-key
+%{_libexecdir}/ipa/ipa-pki-wait-running
 %{_libexecdir}/ipa/ipa-otpd
 %dir %{_libexecdir}/ipa/oddjob
 %attr(0755,root,root) %{_libexecdir}/ipa/oddjob/org.freeipa.server.conncheck
@@ -1477,16 +1149,9 @@ fi
 %{_mandir}/man1/ipa-cacert-manage.1*
 %{_mandir}/man1/ipa-winsync-migrate.1*
 %{_mandir}/man1/ipa-pkinit-manage.1*
+%{_mandir}/man1/ipa-crlgen-manage.1*
+%{_mandir}/man1/ipa-cert-fix.1*
 
-%if 0%{?with_python2} && 0%{?fedora} < 29
-# Fedora 29 workaround: don't build python2-ipaserver, python2-pki is n/a
-%files -n python2-ipaserver
-%doc README.md Contributors.txt
-%license COPYING
-%{python2_sitelib}/ipaserver
-%{python2_sitelib}/ipaserver-*.egg-info
-
-%endif # with_python2 and Fedora < 29
 
 %files -n python3-ipaserver
 %doc README.md Contributors.txt
@@ -1508,6 +1173,7 @@ fi
 # END
 %{_usr}/share/ipa/wsgi.py*
 %{_usr}/share/ipa/kdcproxy.wsgi
+%{_usr}/share/ipa/ipaca*.ini
 %{_usr}/share/ipa/*.ldif
 %{_usr}/share/ipa/*.uldif
 %{_usr}/share/ipa/*.template
@@ -1620,6 +1286,7 @@ fi
 %{_sbindir}/ipa-join
 %{_bindir}/ipa
 %config %{_sysconfdir}/bash_completion.d
+%config %{_sysconfdir}/sysconfig/certmonger
 %{_mandir}/man1/ipa.1*
 %{_mandir}/man1/ipa-getkeytab.1*
 %{_mandir}/man1/ipa-rmkeytab.1*
@@ -1628,25 +1295,11 @@ fi
 %{_mandir}/man1/ipa-certupdate.1*
 %{_mandir}/man1/ipa-join.1*
 
-%if 0%{?with_python2}
-
-%files -n python2-ipaclient
+%files client-samba
 %doc README.md Contributors.txt
 %license COPYING
-%dir %{python_sitelib}/ipaclient
-%{python_sitelib}/ipaclient/*.py*
-%dir %{python_sitelib}/ipaclient/install
-%{python_sitelib}/ipaclient/install/*.py*
-%dir %{python_sitelib}/ipaclient/plugins
-%{python_sitelib}/ipaclient/plugins/*.py*
-%dir %{python_sitelib}/ipaclient/remote_plugins
-%{python_sitelib}/ipaclient/remote_plugins/*.py*
-%dir %{python_sitelib}/ipaclient/remote_plugins/2_*
-%{python_sitelib}/ipaclient/remote_plugins/2_*/*.py*
-# RHEL spec file only: DELETED: Remove csrgen
-%{python_sitelib}/ipaclient-*.egg-info
-
-%endif # with_python2
+%{_sbindir}/ipa-client-samba
+%{_mandir}/man1/ipa-client-samba.1*
 
 %files -n python3-ipaclient
 %doc README.md Contributors.txt
@@ -1699,27 +1352,6 @@ fi
 %doc README.md Contributors.txt
 %license COPYING
 
-%if 0%{?with_python2}
-
-%files -n python2-ipalib
-%doc README.md Contributors.txt
-%license COPYING
-%dir %{python_sitelib}/ipapython
-%{python_sitelib}/ipapython/*.py*
-%dir %{python_sitelib}/ipapython/install
-%{python_sitelib}/ipapython/install/*.py*
-%dir %{python_sitelib}/ipalib
-%{python_sitelib}/ipalib/*.py*
-%dir %{python_sitelib}/ipalib/install
-%{python_sitelib}/ipalib/install/*.py*
-%dir %{python_sitelib}/ipaplatform
-%{python_sitelib}/ipaplatform/*
-%{python_sitelib}/ipapython-*.egg-info
-%{python_sitelib}/ipalib-*.egg-info
-%{python_sitelib}/ipaplatform-*.egg-info
-%{python_sitelib}/ipaplatform-*-nspkg.pth
-
-%endif # with_python2
 
 %files common -f %{gettext_domain}.lang
 %doc README.md Contributors.txt
@@ -1742,56 +1374,95 @@ fi
 
 %if 0%{?with_ipatests}
 
-%if 0%{?with_python2} && 0%{?fedora} < 29
-# Fedora 29 workaround: don't build python2-ipatests, depends on ipaserver
-%files -n python2-ipatests
-%doc README.md Contributors.txt
-%license COPYING
-%{python_sitelib}/ipatests
-%{python_sitelib}/ipatests-*.egg-info
-%{_bindir}/ipa-run-tests-2
-%{_bindir}/ipa-test-config-2
-%{_bindir}/ipa-test-task-2
-%{_bindir}/ipa-run-tests-%{python2_version}
-%{_bindir}/ipa-test-config-%{python2_version}
-%{_bindir}/ipa-test-task-%{python2_version}
-%if %{with_default_python} != 3
-%{_bindir}/ipa-run-tests
-%{_bindir}/ipa-test-config
-%{_bindir}/ipa-test-task
-%{_mandir}/man1/ipa-run-tests.1*
-%{_mandir}/man1/ipa-test-config.1*
-%{_mandir}/man1/ipa-test-task.1*
-%endif
-
-%endif # with_python2 and Fedora < 29
 
 %files -n python3-ipatests
 %doc README.md Contributors.txt
 %license COPYING
 %{python3_sitelib}/ipatests
 %{python3_sitelib}/ipatests-*.egg-info
-%{_bindir}/ipa-run-tests-3
-%{_bindir}/ipa-test-config-3
-%{_bindir}/ipa-test-task-3
-%{_bindir}/ipa-run-tests-%{python3_version}
-%{_bindir}/ipa-test-config-%{python3_version}
-%{_bindir}/ipa-test-task-%{python3_version}
-%if %{with_default_python} == 3
 %{_bindir}/ipa-run-tests
 %{_bindir}/ipa-test-config
 %{_bindir}/ipa-test-task
 %{_mandir}/man1/ipa-run-tests.1*
 %{_mandir}/man1/ipa-test-config.1*
 %{_mandir}/man1/ipa-test-task.1*
-%endif
 
 %endif # with_ipatests
 
 
 %changelog
-* Tue May 07 2019 CentOS Sources <bugs@centos.org> - 4.7.1-11.el8.centos
-- Apply debranding changes
+* Thu Aug 29 2019 Thomas Woerner <twoerner@redhat.com> - 4.8.0-10
+- Fix automount behavior with authselect
+  Resolves: RHBZ#1740167
+
+* Mon Aug 19 2019 Thomas Woerner <twoerner@redhat.com> - 4.8.0-9
+- extdom: unify error code handling especially LDAP_NO_SUCH_OBJECT
+  Resolves: RHBZ#1741530
+
+* Thu Aug 15 2019 Thomas Woerner <twoerner@redhat.com> - 4.8.0-8
+- FreeIPA 4.8.0 tarball lacks two update files that are in git
+  Resolves: RHBZ#1741170
+
+* Tue Aug 13 2019 Thomas Woerner <twoerner@redhat.com> - 4.8.0-7
+- Allow insecure binds for migration
+  Resolves: RHBZ#1731963
+
+* Fri Aug  2 2019 Thomas Woerner <twoerner@redhat.com> - 4.8.0-6
+- Fix --external-ca-profile not passed to CSR
+  Resolves: RHBZ#1731813
+
+* Tue Jul 30 2019 Thomas Woerner <twoerner@redhat.com> - 4.8.0-5
+- Remove posixAccount from service_find search filter
+  Resolves: RHBZ#1731437
+- Fix repeated uninstallation of ipa-client-samba crashes
+  Resolves: RHBZ#1732529
+- WebUI: Add PKINIT status field to 'Configuration' page
+  Resolves: RHBZ#1518153
+
+* Tue Jul 16 2019 Alexander Bokovoy <abokovoy@redhat.com> - 4.8.0-4
+- Fix krb5-kdb-server -> krb5-kdb-version
+  Related: RHBZ#1700121
+
+* Mon Jul 15 2019 Alexander Bokovoy <abokovoy@redhat.com> - 4.8.0-3
+- Make sure ipa-server depends on krb5-kdb-version to pick up
+  right MIT Kerberos KDB ABI
+  Related: RHBZ#1700121
+- User field separator uses '$$' within ipaSELInuxUserMapOrder
+  Fixes: RHBZ#1729099
+
+* Wed Jul  3 2019 Thomas Woerner <twoerner@redhat.com> - 4.8.0-2
+- Fixed kdcproxy_version to 0.4-3
+- Fixed krb5_version to 1.17-7
+  Related: RHBZ#1684528
+
+* Wed Jul  3 2019 Thomas Woerner <twoerner@redhat.com> - 4.8.0-1
+- New upstream release 4.8.0
+  - New subpackage: freeipa-client-samba
+  - Added command ipa-cert-fix with man page
+  - New sysconfdir sysconfig/certmonger
+- Updated pki_version, certmonger_version, sssd_version and kdcproxy_version
+  Related: RHBZ#1684528
+
+* Tue May 21 2019 Alexander Bokovoy <abokovoy@redhat.com> - 4.7.90-3
+- Fix upgrade issue with AD trust when no trust yet established
+  Fixes: RHBZ#1708874
+  Related: RHBZ#1684528
+
+* Thu May  9 2019 Alexander Bokovoy <abokovoy@redhat.com> - 4.7.90-2
+- Require certmonger 0.79.7-1
+  Related: RHBZ#1708095
+
+* Mon May  6 2019 Thomas Woerner <twoerner@redhat.com> - 4.7.90-1
+- Update to 4.7.90-pre1
+  Related: RHBZ#1684528
+- Removed patches 0002 to 0031 as these are upsteram and part of 4.7.90-pre1
+- Added new patches 0001-revert-minssf-defaults.patch and
+  0001-Correct-default-fontawesome-path-broken-by-da2cf1c5.patch
+
+* Tue Apr 16 2019 Alexander Bokovoy <abokovoy@redhat.com> - 4.7.1-12
+- Remove strict dependencies to krb5-server version in order to allow
+  update of krb5 to 1.17 and change dependency to KDB DAL version.
+  Resolves: RHBZ#1700121
 
 * Wed Feb 27 2019 Rob Crittenden <rcritten@redhat.com> - 4.7.1-11
 - Handle NFS configuration file changes. nfs-utils moved the