diff --git a/SOURCES/0005-Add-end-to-end-integration-tests-for-external-IdP.patch b/SOURCES/0005-Add-end-to-end-integration-tests-for-external-IdP.patch
new file mode 100644
index 0000000..700df13
--- /dev/null
+++ b/SOURCES/0005-Add-end-to-end-integration-tests-for-external-IdP.patch
@@ -0,0 +1,346 @@
+From 857713c5a9c8e0b62c06dd92e69c09eeb34b2e99 Mon Sep 17 00:00:00 2001
+From: Anuja More <amore@redhat.com>
+Date: Mon, 23 May 2022 12:26:34 +0530
+Subject: [PATCH] Add end to end integration tests for external IdP
+
+Added tests for HBAC and SUDO rule and other
+test scenarios.
+
+Related : https://pagure.io/freeipa/issue/8805
+Related: https://pagure.io/freeipa/issue/8803
+Related: https://pagure.io/freeipa/issue/8804
+
+Signed-off-by: Anuja More <amore@redhat.com>
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ ipatests/test_integration/test_idp.py | 260 ++++++++++++++++++++++----
+ 1 file changed, 226 insertions(+), 34 deletions(-)
+
+diff --git a/ipatests/test_integration/test_idp.py b/ipatests/test_integration/test_idp.py
+index 8f9e92e6a..2ffe6a208 100644
+--- a/ipatests/test_integration/test_idp.py
++++ b/ipatests/test_integration/test_idp.py
+@@ -1,6 +1,8 @@
+ from __future__ import absolute_import
+ 
+ import time
++import pytest
++import re
+ 
+ import textwrap
+ from ipaplatform.paths import paths
+@@ -22,12 +24,12 @@ driver.get(verification_uri)
+ try:
+     element = WebDriverWait(driver, 90).until(
+         EC.presence_of_element_located((By.ID, "username")))
+-    driver.find_element_by_id("username").send_keys("testuser1")
+-    driver.find_element_by_id("password").send_keys("{passwd}")
+-    driver.find_element_by_id("kc-login").click()
++    driver.find_element(By.ID, "username").send_keys("testuser1")
++    driver.find_element(By.ID, "password").send_keys("{passwd}")
++    driver.find_element(By.ID, "kc-login").click()
+     element = WebDriverWait(driver, 90).until(
+         EC.presence_of_element_located((By.ID, "kc-login")))
+-    driver.find_element_by_id("kc-login").click()
++    driver.find_element(By.ID, "kc-login").click()
+     assert "Device Login Successful" in driver.page_source
+ finally:
+     now = datetime.now().strftime("%M-%S")
+@@ -39,18 +41,12 @@ finally:
+ def add_user_code(host, verification_uri):
+     contents = user_code_script.format(uri=verification_uri,
+                                        passwd=host.config.admin_password)
+-    host.put_file_contents("/tmp/add_user_code.py", contents)
+-    tasks.run_repeatedly(
+-        host, ['python3', '/tmp/add_user_code.py'])
+-
+-
+-def get_verification_uri(host, since, keycloak_server_name):
+-    command = textwrap.dedent("""
+-    journalctl -u ipa-otpd\\* --since="%s" | grep "user_code:" | awk '{ print substr($7,2,9) }'""" % since)  # noqa: E501
+-    user_code = host.run_command(command).stdout_text.rstrip("\r\n")
+-    uri = ("https://{0}:8443/auth/realms/master/device?user_code={1}".format(
+-        keycloak_server_name, user_code))
+-    return uri
++    try:
++        host.put_file_contents("/tmp/add_user_code.py", contents)
++        tasks.run_repeatedly(
++            host, ['python3', '/tmp/add_user_code.py'])
++    finally:
++        host.run_command(["rm", "-f", "/tmp/add_user_code.py"])
+ 
+ 
+ def kinit_idp(host, user, keycloak_server):
+@@ -58,11 +54,14 @@ def kinit_idp(host, user, keycloak_server):
+     tasks.kdestroy_all(host)
+     # create armor for FAST
+     host.run_command(["kinit", "-n", "-c", ARMOR])
+-    since = time.strftime('%Y-%m-%d %H:%M:%S')
+     cmd = ["kinit", "-T", ARMOR, user]
++
+     with host.spawn_expect(cmd, default_timeout=100) as e:
+-        e.expect('Authenticate at .+: ')
+-        uri = get_verification_uri(host, since, keycloak_server.hostname)
++        e.expect('Authenticate at (.+) and press ENTER.:')
++        prompt = e.get_last_output()
++        uri = re.search(r'Authenticate at (.*?) and press ENTER.:', prompt
++                        ).group(1)
++        time.sleep(15)
+         if uri:
+             add_user_code(keycloak_server, uri)
+         e.sendline('\n')
+@@ -74,21 +73,27 @@ def kinit_idp(host, user, keycloak_server):
+ 
+ class TestIDPKeycloak(IntegrationTest):
+ 
+-    num_replicas = 1
++    num_replicas = 2
+     topology = 'line'
+ 
+     @classmethod
+     def install(cls, mh):
+-        tasks.install_master(cls.master, setup_dns=True)
+-        tasks.install_client(cls.master, cls.replicas[0])
+-        content = cls.master.get_file_contents(paths.IPA_DEFAULT_CONF,
+-                                               encoding='utf-8')
+-        new_content = content + "\noidc_child_debug_level = 10"
+-        cls.master.put_file_contents(paths.IPA_DEFAULT_CONF, new_content)
++        cls.client = cls.replicas[0]
++        cls.replica = cls.replicas[1]
++        tasks.install_master(cls.master)
++        tasks.install_client(cls.master, cls.replicas[0],
++                             extra_args=["--mkhomedir"])
++        tasks.install_replica(cls.master, cls.replicas[1])
++        for host in [cls.master, cls.replicas[0], cls.replicas[1]]:
++            content = host.get_file_contents(paths.IPA_DEFAULT_CONF,
++                                             encoding='utf-8')
++            new_content = content + "\noidc_child_debug_level = 10"
++            host.put_file_contents(paths.IPA_DEFAULT_CONF, new_content)
+         with tasks.remote_sssd_config(cls.master) as sssd_config:
+             sssd_config.edit_domain(
+                 cls.master.domain, 'krb5_auth_timeout', 1100)
+         tasks.clear_sssd_cache(cls.master)
++        tasks.clear_sssd_cache(cls.replicas[0])
+         tasks.kinit_admin(cls.master)
+         cls.master.run_command(["ipa", "config-mod", "--user-auth-type=idp",
+                                 "--user-auth-type=password"])
+@@ -97,20 +102,207 @@ class TestIDPKeycloak(IntegrationTest):
+         cls.replicas[0].run_command(xvfb)
+ 
+     def test_auth_keycloak_idp(self):
+-        keycloak_srv = self.replicas[0]
+-        create_quarkus.setup_keycloakserver(keycloak_srv)
++        """
++        Test case to check that OAuth 2.0 Device
++        Authorization Grant is working as
++        expected for user configured with external idp.
++        """
++        create_quarkus.setup_keycloakserver(self.client)
+         time.sleep(60)
+-        create_quarkus.setup_keycloak_client(keycloak_srv)
++        create_quarkus.setup_keycloak_client(self.client)
+         tasks.kinit_admin(self.master)
+-        cmd = ["ipa", "idp-add", "keycloak", "--provider=keycloak",
++        cmd = ["ipa", "idp-add", "keycloakidp", "--provider=keycloak",
+                "--client-id=ipa_oidc_client", "--org=master",
+-               "--base-url={0}:8443/auth".format(keycloak_srv.hostname)]
++               "--base-url={0}:8443/auth".format(self.client.hostname)]
+         self.master.run_command(cmd, stdin_text="{0}\n{0}".format(
+-            keycloak_srv.config.admin_password))
++            self.client.config.admin_password))
+         tasks.user_add(self.master, 'keycloakuser',
+                        extra_args=["--user-auth-type=idp",
+                                    "--idp-user-id=testuser1@ipa.test",
+-                                   "--idp=keycloak"]
++                                   "--idp=keycloakidp"]
+                        )
++        list_user = self.master.run_command(
++            ["ipa", "user-find", "--idp-user-id=testuser1@ipa.test"]
++        )
++        assert "keycloakuser" in list_user.stdout_text
++        list_by_idp = self.master.run_command(["ipa", "user-find",
++                                               "--idp=keycloakidp"]
++                                              )
++        assert "keycloakuser" in list_by_idp.stdout_text
++        list_by_user = self.master.run_command(
++            ["ipa", "user-find", "--idp-user-id=testuser1@ipa.test", "--all"]
++        )
++        assert "keycloakidp" in list_by_user.stdout_text
++        tasks.clear_sssd_cache(self.master)
++        kinit_idp(self.master, 'keycloakuser', keycloak_server=self.client)
++
++    @pytest.fixture
++    def hbac_setup_teardown(self):
++        # allow sshd only on given host
++        tasks.kinit_admin(self.master)
++        self.master.run_command(["ipa", "hbacrule-disable", "allow_all"])
++        self.master.run_command(["ipa", "hbacrule-add", "rule1"])
++        self.master.run_command(["ipa", "hbacrule-add-user", "rule1",
++                                 "--users=keycloakuser"]
++                                )
++        self.master.run_command(["ipa", "hbacrule-add-host", "rule1",
++                                 "--hosts", self.replica.hostname])
++        self.master.run_command(["ipa", "hbacrule-add-service", "rule1",
++                                 "--hbacsvcs=sshd"]
++                                )
++        tasks.clear_sssd_cache(self.master)
++        tasks.clear_sssd_cache(self.replica)
++        yield
++
++        # cleanup
++        tasks.kinit_admin(self.master)
++        self.master.run_command(["ipa", "hbacrule-enable", "allow_all"])
++        self.master.run_command(["ipa", "hbacrule-del", "rule1"])
++
++    def test_auth_hbac(self, hbac_setup_teardown):
++        """
++        Test case to check that hbacrule is working as
++        expected for user configured with external idp.
++        """
++        kinit_idp(self.master, 'keycloakuser', keycloak_server=self.client)
++        ssh_cmd = "ssh -q -K -l keycloakuser {0} whoami"
++        valid_ssh = self.master.run_command(
++            ssh_cmd.format(self.replica.hostname))
++        assert "keycloakuser" in valid_ssh.stdout_text
++        negative_ssh = self.master.run_command(
++            ssh_cmd.format(self.master.hostname), raiseonerr=False
++        )
++        assert negative_ssh.returncode == 255
++
++    def test_auth_sudo_idp(self):
++        """
++        Test case to check that sudorule is working as
++        expected for user configured with external idp.
++        """
++        tasks.kdestroy_all(self.master)
++        tasks.kinit_admin(self.master)
++        #  rule: keycloakuser are allowed to execute yum on
++        #  the client machine as root.
++        cmdlist = [
++            ["ipa", "sudocmd-add", "/usr/bin/yum"],
++            ["ipa", "sudorule-add", "sudorule"],
++            ['ipa', 'sudorule-add-user', '--users=keycloakuser',
++             'sudorule'],
++            ['ipa', 'sudorule-add-host', '--hosts',
++             self.client.hostname, 'sudorule'],
++            ['ipa', 'sudorule-add-runasuser',
++             '--users=root', 'sudorule'],
++            ['ipa', 'sudorule-add-allow-command',
++             '--sudocmds=/usr/bin/yum', 'sudorule'],
++            ['ipa', 'sudorule-show', 'sudorule', '--all'],
++            ['ipa', 'sudorule-add-option',
++             'sudorule', '--sudooption', "!authenticate"]
++        ]
++        for cmd in cmdlist:
++            self.master.run_command(cmd)
++        tasks.clear_sssd_cache(self.master)
++        tasks.clear_sssd_cache(self.client)
++        try:
++            cmd = 'sudo -ll -U keycloakuser'
++            test = self.client.run_command(cmd).stdout_text
++            assert "User keycloakuser may run the following commands" in test
++            assert "/usr/bin/yum" in test
++            kinit_idp(self.client, 'keycloakuser', self.client)
++            test_sudo = 'su -c "sudo yum list wget" keycloakuser'
++            self.client.run_command(test_sudo)
++            list_fail = self.master.run_command(cmd).stdout_text
++            assert "User keycloakuser is not allowed to run sudo" in list_fail
++        finally:
++            tasks.kinit_admin(self.master)
++            self.master.run_command(['ipa', 'sudorule-del', 'sudorule'])
++            self.master.run_command(["ipa", "sudocmd-del", "/usr/bin/yum"])
++
++    def test_auth_replica(self):
++        """
++        Test case to check that OAuth 2.0 Device
++        Authorization is working as expected on replica.
++        """
++        tasks.clear_sssd_cache(self.master)
++        tasks.clear_sssd_cache(self.replica)
++        tasks.kinit_admin(self.replica)
++        list_user = self.master.run_command(
++            ["ipa", "user-find", "--idp-user-id=testuser1@ipa.test"]
++        )
++        assert "keycloakuser" in list_user.stdout_text
++        list_by_idp = self.replica.run_command(["ipa", "user-find",
++                                                "--idp=keycloakidp"]
++                                               )
++        assert "keycloakuser" in list_by_idp.stdout_text
++        list_by_user = self.replica.run_command(
++            ["ipa", "user-find", "--idp-user-id=testuser1@ipa.test", "--all"]
++        )
++        assert "keycloakidp" in list_by_user.stdout_text
++        kinit_idp(self.replica, 'keycloakuser', keycloak_server=self.client)
++
++    def test_idp_with_services(self):
++        """
++        Test case to check that services can be configured
++        auth indicator as idp.
++        """
+         tasks.clear_sssd_cache(self.master)
+-        kinit_idp(self.master, 'keycloakuser', keycloak_srv)
++        tasks.kinit_admin(self.master)
++        domain = self.master.domain.name.upper()
++        services = [
++            "DNS/{0}@{1}".format(self.master.hostname, domain),
++            "HTTP/{0}@{1}".format(self.client.hostname, domain),
++            "dogtag/{0}@{1}".format(self.master.hostname, domain),
++            "ipa-dnskeysyncd/{0}@{1}".format(self.master.hostname, domain)
++        ]
++        try:
++            for service in services:
++                test = self.master.run_command(["ipa", "service-mod", service,
++                                                "--auth-ind=idp"]
++                                               )
++                assert "Authentication Indicators: idp" in test.stdout_text
++        finally:
++            for service in services:
++                self.master.run_command(["ipa", "service-mod", service,
++                                         "--auth-ind="])
++
++    def test_idp_backup_restore(self):
++        """
++        Test case to check that after restore data is retrieved
++        with related idp configuration.
++        """
++        tasks.kinit_admin(self.master)
++        user = "backupuser"
++        cmd = ["ipa", "idp-add", "testidp", "--provider=keycloak",
++               "--client-id=ipa_oidc_client", "--org=master",
++               "--base-url={0}:8443/auth".format(self.client.hostname)]
++        self.master.run_command(cmd, stdin_text="{0}\n{0}".format(
++            self.client.config.admin_password))
++
++        tasks.user_add(self.master, user,
++                       extra_args=["--user-auth-type=idp",
++                                   "--idp-user-id=testuser1@ipa.test",
++                                   "--idp=testidp"]
++                       )
++
++        backup_path = tasks.get_backup_dir(self.master)
++        # change data after backup
++        self.master.run_command(['ipa', 'user-del', user])
++        self.master.run_command(['ipa', 'idp-del', 'testidp'])
++        dirman_password = self.master.config.dirman_password
++        self.master.run_command(['ipa-restore', backup_path],
++                                stdin_text=dirman_password + '\nyes')
++        try:
++            list_user = self.master.run_command(
++                ['ipa', 'user-show', 'backupuser', '--all']
++            ).stdout_text
++            assert "External IdP configuration: testidp" in list_user
++            assert "User authentication types: idp" in list_user
++            assert ("External IdP user identifier: "
++                    "testuser1@ipa.test") in list_user
++            list_idp = self.master.run_command(['ipa', 'idp-find', 'testidp'])
++            assert "testidp" in list_idp.stdout_text
++            kinit_idp(self.master, user, self.client)
++        finally:
++            tasks.kdestroy_all(self.master)
++            tasks.kinit_admin(self.master)
++            self.master.run_command(["rm", "-rf", backup_path])
++            self.master.run_command(["ipa", "idp-del", "testidp"])
+-- 
+2.36.1
+
diff --git a/SOURCES/0006-webui-Do-not-allow-empty-pagination-size_rhbz#2094672.patch b/SOURCES/0006-webui-Do-not-allow-empty-pagination-size_rhbz#2094672.patch
new file mode 100644
index 0000000..25e9f72
--- /dev/null
+++ b/SOURCES/0006-webui-Do-not-allow-empty-pagination-size_rhbz#2094672.patch
@@ -0,0 +1,67 @@
+From 991849cf58fa990ad4540a61214b5ab4fcd4baa1 Mon Sep 17 00:00:00 2001
+From: Armando Neto <abiagion@redhat.com>
+Date: Fri, 8 Jul 2022 15:56:31 -0300
+Subject: [PATCH] webui: Do not allow empty pagination size
+
+Pagination size must be required, the current validators are triggered after
+form is submitted, thus the only way for check if data is not empty is by making
+the field required.
+
+Fixes: https://pagure.io/freeipa/issue/9192
+
+Signed-off-by: Armando Neto <abiagion@redhat.com>
+Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
+---
+ .../ui/src/freeipa/Application_controller.js  |  1 +
+ ipatests/test_webui/test_misc_cases.py        | 19 +++++++++++++++++++
+ 2 files changed, 20 insertions(+)
+
+diff --git a/install/ui/src/freeipa/Application_controller.js b/install/ui/src/freeipa/Application_controller.js
+index 46aabc9c4..140ee8fe0 100644
+--- a/install/ui/src/freeipa/Application_controller.js
++++ b/install/ui/src/freeipa/Application_controller.js
+@@ -318,6 +318,7 @@ define([
+                         $type: 'text',
+                         name: 'pagination_size',
+                         label: '@i18n:customization.table_pagination',
++                        required: true,
+                         validators: ['positive_integer']
+                     }
+                 ]
+diff --git a/ipatests/test_webui/test_misc_cases.py b/ipatests/test_webui/test_misc_cases.py
+index 5f7ffb54e..aca9e1a99 100644
+--- a/ipatests/test_webui/test_misc_cases.py
++++ b/ipatests/test_webui/test_misc_cases.py
+@@ -11,6 +11,11 @@ from ipatests.test_webui.ui_driver import screenshot
+ import pytest
+ import re
+ 
++try:
++    from selenium.webdriver.common.by import By
++except ImportError:
++    pass
++
+ 
+ @pytest.mark.tier1
+ class TestMiscCases(UI_driver):
+@@ -26,3 +31,17 @@ class TestMiscCases(UI_driver):
+         ver_re = re.compile('version: .*')
+         assert re.search(ver_re, about_text), 'Version not found'
+         self.dialog_button_click('ok')
++
++    @screenshot
++    def test_customization_pagination_input_required(self):
++        """Test if 'pagination size' is required when submitting the form."""
++        self.init_app()
++
++        self.profile_menu_action('configuration')
++        self.fill_input('pagination_size', '')
++        self.dialog_button_click('save')
++
++        pagination_size_elem = self.find(
++            ".widget[name='pagination_size']", By.CSS_SELECTOR)
++
++        self.assert_field_validation_required(parent=pagination_size_elem)
+-- 
+2.36.1
+
diff --git a/SOURCES/0007-webui-Allow-grace-login-limit_rhbz#2109243.patch b/SOURCES/0007-webui-Allow-grace-login-limit_rhbz#2109243.patch
new file mode 100644
index 0000000..93cfab9
--- /dev/null
+++ b/SOURCES/0007-webui-Allow-grace-login-limit_rhbz#2109243.patch
@@ -0,0 +1,56 @@
+From ade5093b08f92b279c200f341e96972a74f644d8 Mon Sep 17 00:00:00 2001
+From: Carla Martinez <carlmart@redhat.com>
+Date: Fri, 29 Jul 2022 13:16:16 +0200
+Subject: [PATCH] webui: Allow grace login limit
+
+There was no support for setting the grace login limit on the WebUI. The
+only way to so was only via CLI:
+
+   `ipa pwpolicy-mod --gracelimit=2 global_policy`
+
+Thus, the grace login limit must be updated from the policy section and
+this will reflect also on the user settings (under the 'Password Policy'
+section)
+
+Fixes: https://pagure.io/freeipa/issue/9211
+
+Signed-off-by: Carla Martinez <carlmart@redhat.com>
+Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
+---
+ install/ui/src/freeipa/policy.js | 3 +++
+ install/ui/src/freeipa/user.js   | 5 +++++
+ 2 files changed, 8 insertions(+)
+
+diff --git a/install/ui/src/freeipa/policy.js b/install/ui/src/freeipa/policy.js
+index fa2028a52..7ec103636 100644
+--- a/install/ui/src/freeipa/policy.js
++++ b/install/ui/src/freeipa/policy.js
+@@ -72,6 +72,9 @@ return {
+                         {
+                             name: 'cospriority',
+                             required: true
++                        },
++                        {
++                            name: 'passwordgracelimit'
+                         }
+                     ]
+                 }]
+diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js
+index a580db035..b47c97f72 100644
+--- a/install/ui/src/freeipa/user.js
++++ b/install/ui/src/freeipa/user.js
+@@ -318,6 +318,11 @@ return {
+                             label: '@mo-param:pwpolicy:krbpwdlockoutduration:label',
+                             read_only: true,
+                             measurement_unit: 'seconds'
++                        },
++                        {
++                            name: 'passwordgracelimit',
++                            label: '@mo-param:pwpolicy:passwordgracelimit:label',
++                            read_only: true
+                         }
+                     ]
+                 },
+-- 
+2.37.2
+
diff --git a/SOURCES/0008-check_repl_update-in-progress-is-a-boolean_rhbz#2117303.patch b/SOURCES/0008-check_repl_update-in-progress-is-a-boolean_rhbz#2117303.patch
new file mode 100644
index 0000000..36629da
--- /dev/null
+++ b/SOURCES/0008-check_repl_update-in-progress-is-a-boolean_rhbz#2117303.patch
@@ -0,0 +1,35 @@
+From 05a298f56485222583cb7dd4f6a3a4c5c77fc8cf Mon Sep 17 00:00:00 2001
+From: Florence Blanc-Renaud <flo@redhat.com>
+Date: Sun, 7 Aug 2022 12:44:47 +0200
+Subject: [PATCH] check_repl_update: in progress is a boolean
+
+With the fix for https://pagure.io/freeipa/issue/9171,
+nsds5replicaUpdateInProgress is now handled as a boolean.
+One remaining occurrence was still handling it as a string
+and calling lower() on its value.
+
+Replace with direct boolean comparison.
+
+Fixes: https://pagure.io/freeipa/issue/9218
+Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
+Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
+---
+ ipaserver/install/replication.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
+index 16be3760c..9d9aa1c4b 100644
+--- a/ipaserver/install/replication.py
++++ b/ipaserver/install/replication.py
+@@ -1152,7 +1152,7 @@ class ReplicationManager:
+             except (ValueError, TypeError, KeyError):
+                 end = 0
+             # incremental update is done if inprogress is false and end >= start
+-            done = inprogress and inprogress.lower() == 'false' and start <= end
++            done = inprogress is not None and not inprogress and start <= end
+             logger.info("Replication Update in progress: %s: status: %s: "
+                         "start: %d: end: %d",
+                         inprogress, status, start, end)
+-- 
+2.37.2
+
diff --git a/SOURCES/0009-Disabling-gracelimit-does-not-prevent-LDAP-binds_rhbz#2109236.patch b/SOURCES/0009-Disabling-gracelimit-does-not-prevent-LDAP-binds_rhbz#2109236.patch
new file mode 100644
index 0000000..17088cf
--- /dev/null
+++ b/SOURCES/0009-Disabling-gracelimit-does-not-prevent-LDAP-binds_rhbz#2109236.patch
@@ -0,0 +1,125 @@
+From 1316cd8b2252c2543cf2ef2186956a8833037b1e Mon Sep 17 00:00:00 2001
+From: Rob Crittenden <rcritten@redhat.com>
+Date: Thu, 21 Jul 2022 09:28:46 -0400
+Subject: [PATCH] Disabling gracelimit does not prevent LDAP binds
+
+Originally the code treated 0 as disabled. This was
+changed during the review process to -1 but one remnant
+was missed effetively allowing gracelimit 0 to also mean
+disabled.
+
+Add explicit tests for testing with gracelimit = 0 and
+gracelimit = -1.
+
+Also remove some extranous "str(self.master.domain.basedn)"
+lines from some of the tests.
+
+Fixes: https://pagure.io/freeipa/issue/9206
+
+Signed-off-by: Rob Crittenden <rcritten@redhat.com>
+Reviewed-By: Francisco Trivino <ftrivino@redhat.com>
+---
+ .../ipa-graceperiod/ipa_graceperiod.c         |  2 +-
+ ipatests/test_integration/test_pwpolicy.py    | 55 ++++++++++++++++++-
+ 2 files changed, 53 insertions(+), 4 deletions(-)
+
+diff --git a/daemons/ipa-slapi-plugins/ipa-graceperiod/ipa_graceperiod.c b/daemons/ipa-slapi-plugins/ipa-graceperiod/ipa_graceperiod.c
+index a3f57cb4b..345e1dee7 100644
+--- a/daemons/ipa-slapi-plugins/ipa-graceperiod/ipa_graceperiod.c
++++ b/daemons/ipa-slapi-plugins/ipa-graceperiod/ipa_graceperiod.c
+@@ -479,7 +479,7 @@ static int ipagraceperiod_preop(Slapi_PBlock *pb)
+         if (pwresponse_requested) {
+             slapi_pwpolicy_make_response_control(pb, -1, grace_limit - grace_user_time , -1);
+         }
+-    } else if ((grace_limit > 0) && (grace_user_time >= grace_limit)) {
++    } else if (grace_user_time >= grace_limit) {
+         LOG_TRACE("%s password is expired and out of grace limit\n", dn);
+         errstr = "Password is expired.\n";
+         ret = LDAP_INVALID_CREDENTIALS;
+diff --git a/ipatests/test_integration/test_pwpolicy.py b/ipatests/test_integration/test_pwpolicy.py
+index 6d6698284..41d6e9070 100644
+--- a/ipatests/test_integration/test_pwpolicy.py
++++ b/ipatests/test_integration/test_pwpolicy.py
+@@ -36,7 +36,7 @@ class TestPWPolicy(IntegrationTest):
+         cls.master.run_command(['ipa', 'group-add-member', POLICY,
+                                 '--users', USER])
+         cls.master.run_command(['ipa', 'pwpolicy-add', POLICY,
+-                                '--priority', '1'])
++                                '--priority', '1', '--gracelimit', '-1'])
+         cls.master.run_command(['ipa', 'passwd', USER],
+                                stdin_text='{password}\n{password}\n'.format(
+                                password=PASSWORD
+@@ -265,7 +265,6 @@ class TestPWPolicy(IntegrationTest):
+ 
+     def test_graceperiod_expired(self):
+         """Test the LDAP bind grace period"""
+-        str(self.master.domain.basedn)
+         dn = "uid={user},cn=users,cn=accounts,{base_dn}".format(
+              user=USER, base_dn=str(self.master.domain.basedn))
+ 
+@@ -308,7 +307,6 @@ class TestPWPolicy(IntegrationTest):
+ 
+     def test_graceperiod_not_replicated(self):
+         """Test that the grace period is reset on password reset"""
+-        str(self.master.domain.basedn)
+         dn = "uid={user},cn=users,cn=accounts,{base_dn}".format(
+              user=USER, base_dn=str(self.master.domain.basedn))
+ 
+@@ -341,3 +339,54 @@ class TestPWPolicy(IntegrationTest):
+         )
+         assert 'passwordgraceusertime: 0' in result.stdout_text.lower()
+         self.reset_password(self.master)
++
++    def test_graceperiod_zero(self):
++        """Test the LDAP bind with zero grace period"""
++        dn = "uid={user},cn=users,cn=accounts,{base_dn}".format(
++             user=USER, base_dn=str(self.master.domain.basedn))
++
++        self.master.run_command(
++            ["ipa", "pwpolicy-mod", POLICY, "--gracelimit", "0", ],
++        )
++
++        # Resetting the password will mark it as expired
++        self.reset_password(self.master)
++
++        # Now grace is done and binds should fail.
++        result = self.master.run_command(
++            ["ldapsearch", "-e", "ppolicy", "-D", dn,
++             "-w", PASSWORD, "-b", dn], raiseonerr=False
++        )
++        assert result.returncode == 49
++
++        assert 'Password is expired' in result.stderr_text
++        assert 'Password expired, 0 grace logins remain' in result.stderr_text
++
++    def test_graceperiod_disabled(self):
++        """Test the LDAP bind with grace period disabled (-1)"""
++        str(self.master.domain.basedn)
++        dn = "uid={user},cn=users,cn=accounts,{base_dn}".format(
++             user=USER, base_dn=str(self.master.domain.basedn))
++
++        # This can fail if gracelimit is already -1 so ignore it
++        self.master.run_command(
++            ["ipa", "pwpolicy-mod", POLICY, "--gracelimit", "-1",],
++            raiseonerr=False,
++        )
++
++        # Ensure the password is expired
++        self.reset_password(self.master)
++
++        result = self.kinit_as_user(self.master, PASSWORD, PASSWORD)
++
++        for _i in range(0, 10):
++            result = self.master.run_command(
++                ["ldapsearch", "-e", "ppolicy", "-D", dn,
++                 "-w", PASSWORD, "-b", dn]
++            )
++
++        # With graceperiod disabled it should not increment
++        result = tasks.ldapsearch_dm(
++            self.master, dn, ['passwordgraceusertime',],
++        )
++        assert 'passwordgraceusertime: 0' in result.stdout_text.lower()
+-- 
+2.37.2
+
diff --git a/SOURCES/0010-Set-passwordgracelimit-to-match-global-policy-on-group-pw-policies_rhbz#2115475.patch b/SOURCES/0010-Set-passwordgracelimit-to-match-global-policy-on-group-pw-policies_rhbz#2115475.patch
new file mode 100644
index 0000000..952e49b
--- /dev/null
+++ b/SOURCES/0010-Set-passwordgracelimit-to-match-global-policy-on-group-pw-policies_rhbz#2115475.patch
@@ -0,0 +1,230 @@
+From 434620ee342ac4767beccec647a318bfa7743dfa Mon Sep 17 00:00:00 2001
+From: Rob Crittenden <rcritten@redhat.com>
+Date: Thu, 18 Aug 2022 08:21:58 -0400
+Subject: [PATCH] doc: Update LDAP grace period design with default values
+
+New group password policies will get -1 (unlimited) on creation
+by default.
+
+Existing group password policies will remain untouched and
+those created prior will be treated as no BIND allowed.
+
+Fixes: https://pagure.io/freeipa/issue/9212
+
+Signed-off-by: Rob Crittenden <rcritten@redhat.com>
+Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
+---
+ doc/designs/ldap_grace_period.md | 17 ++++++++++++++++-
+ 1 file changed, 16 insertions(+), 1 deletion(-)
+
+diff --git a/doc/designs/ldap_grace_period.md b/doc/designs/ldap_grace_period.md
+index 4b9db3424..e26aedda9 100644
+--- a/doc/designs/ldap_grace_period.md
++++ b/doc/designs/ldap_grace_period.md
+@@ -51,7 +51,22 @@ The basic flow is:
+ 
+ On successful password reset (by anyone) reset the user's passwordGraceUserTime to 0.
+ 
+-The default value on install/upgrade will be -1 to retail existing behavior.
++Range values for passwordgracelimit are:
++
++-1 : password grace checking is disabled
++ 0 : no grace BIND are allowed at all post-expiration
++ 1..MAXINT: the number of BIND allowed post-expiration
++
++The default value for the global policy on install/upgrade will be -1 to
++retain existing behavior.
++
++New group password policies will default to -1 to retain previous
++behavior.
++
++Existing group policies with no grace limit set are updated to use
++the default unlimited value, -1. This is done because lack of value in
++LDAP is treated as 0 so any existing group policies would not allow
++post-expiration BIND so this will avoid confusion.
+ 
+ The per-user attempts will not be replicated.
+ 
+-- 
+2.37.2
+
+From 497a57e7a6872fa30d1855a1d91a455bfdbf9300 Mon Sep 17 00:00:00 2001
+From: Rob Crittenden <rcritten@redhat.com>
+Date: Thu, 4 Aug 2022 12:04:22 -0400
+Subject: [PATCH] Set default gracelimit on group password policies to -1
+
+This will retain previous behavior of unlimited LDAP BIND
+post-expiration.
+
+Fixes: https://pagure.io/freeipa/issue/9212
+
+Signed-off-by: Rob Crittenden <rcritten@redhat.com>
+Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
+---
+ API.txt                                      | 2 +-
+ ipaserver/plugins/pwpolicy.py                | 2 ++
+ ipatests/test_xmlrpc/test_pwpolicy_plugin.py | 2 ++
+ 3 files changed, 5 insertions(+), 1 deletion(-)
+
+diff --git a/API.txt b/API.txt
+index 5ba9add13..d7ea74f08 100644
+--- a/API.txt
++++ b/API.txt
+@@ -4075,7 +4075,7 @@ option: Int('krbpwdlockoutduration?', cli_name='lockouttime')
+ option: Int('krbpwdmaxfailure?', cli_name='maxfail')
+ option: Int('krbpwdmindiffchars?', cli_name='minclasses')
+ option: Int('krbpwdminlength?', cli_name='minlength')
+-option: Int('passwordgracelimit?', cli_name='gracelimit', default=-1)
++option: Int('passwordgracelimit?', autofill=True, cli_name='gracelimit', default=-1)
+ option: Flag('raw', autofill=True, cli_name='raw', default=False)
+ option: Str('setattr*', cli_name='setattr')
+ option: Str('version?')
+diff --git a/ipaserver/plugins/pwpolicy.py b/ipaserver/plugins/pwpolicy.py
+index 4428aede2..f4ebffd5c 100644
+--- a/ipaserver/plugins/pwpolicy.py
++++ b/ipaserver/plugins/pwpolicy.py
+@@ -408,6 +408,7 @@ class pwpolicy(LDAPObject):
+             minvalue=-1,
+             maxvalue=Int.MAX_UINT32,
+             default=-1,
++            autofill=True,
+         ),
+     )
+ 
+@@ -539,6 +540,7 @@ class pwpolicy_add(LDAPCreate):
+             keys[-1], krbpwdpolicyreference=dn,
+             cospriority=options.get('cospriority')
+         )
++
+         return dn
+ 
+     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+diff --git a/ipatests/test_xmlrpc/test_pwpolicy_plugin.py b/ipatests/test_xmlrpc/test_pwpolicy_plugin.py
+index 8eee69c18..fc785223b 100644
+--- a/ipatests/test_xmlrpc/test_pwpolicy_plugin.py
++++ b/ipatests/test_xmlrpc/test_pwpolicy_plugin.py
+@@ -387,6 +387,7 @@ class test_pwpolicy_mod_cospriority(Declarative):
+                     krbpwdhistorylength=[u'10'],
+                     krbpwdmindiffchars=[u'3'],
+                     krbpwdminlength=[u'8'],
++                    passwordgracelimit=[u'-1'],
+                     objectclass=objectclasses.pwpolicy,
+                 ),
+                 summary=None,
+@@ -417,6 +418,7 @@ class test_pwpolicy_mod_cospriority(Declarative):
+                     krbpwdhistorylength=[u'10'],
+                     krbpwdmindiffchars=[u'3'],
+                     krbpwdminlength=[u'8'],
++                    passwordgracelimit=[u'-1'],
+                 ),
+                 summary=None,
+                 value=u'ipausers',
+-- 
+2.37.2
+
+From a4ddaaf3048c4e8d78a1807af7266ee40ab3a30b Mon Sep 17 00:00:00 2001
+From: Rob Crittenden <rcritten@redhat.com>
+Date: Thu, 4 Aug 2022 12:04:41 -0400
+Subject: [PATCH] Set default on group pwpolicy with no grace limit in upgrade
+
+If an existing group policy lacks a password grace limit
+update it to -1 on upgrade.
+
+Fixes: https://pagure.io/freeipa/issue/9212
+
+Signed-off-by: Rob Crittenden <rcritten@redhat.com>
+Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
+---
+ .../updates/90-post_upgrade_plugins.update    |  1 +
+ ipaserver/install/plugins/update_pwpolicy.py  | 66 +++++++++++++++++++
+ 2 files changed, 67 insertions(+)
+
+diff --git a/install/updates/90-post_upgrade_plugins.update b/install/updates/90-post_upgrade_plugins.update
+index c7ec71d49..6fe91aa6c 100644
+--- a/install/updates/90-post_upgrade_plugins.update
++++ b/install/updates/90-post_upgrade_plugins.update
+@@ -26,6 +26,7 @@ plugin: update_ra_cert_store
+ plugin: update_mapping_Guests_to_nobody
+ plugin: fix_kra_people_entry
+ plugin: update_pwpolicy
++plugin: update_pwpolicy_grace
+ 
+ # last
+ # DNS version 1
+diff --git a/ipaserver/install/plugins/update_pwpolicy.py b/ipaserver/install/plugins/update_pwpolicy.py
+index dca44ce43..4185f0343 100644
+--- a/ipaserver/install/plugins/update_pwpolicy.py
++++ b/ipaserver/install/plugins/update_pwpolicy.py
+@@ -78,3 +78,69 @@ class update_pwpolicy(Updater):
+                 return False, []
+ 
+         return False, []
++
++
++@register()
++class update_pwpolicy_grace(Updater):
++    """
++    Ensure all group policies have a grace period set.
++    """
++
++    def execute(self, **options):
++        ldap = self.api.Backend.ldap2
++
++        base_dn = DN(('cn', self.api.env.realm), ('cn', 'kerberos'),
++                     self.api.env.basedn)
++        search_filter = (
++            "(&(objectClass=krbpwdpolicy)(!(passwordgracelimit=*)))"
++        )
++
++        while True:
++            # Run the search in loop to avoid issues when LDAP limits are hit
++            # during update
++
++            try:
++                (entries, truncated) = ldap.find_entries(
++                    search_filter, ['objectclass'], base_dn, time_limit=0,
++                    size_limit=0)
++
++            except errors.EmptyResult:
++                logger.debug("update_pwpolicy: no policies without "
++                             "passwordgracelimit set")
++                return False, []
++
++            except errors.ExecutionError as e:
++                logger.error("update_pwpolicy: cannot retrieve list "
++                             "of policies missing passwordgracelimit: %s", e)
++                return False, []
++
++            logger.debug("update_pwpolicy: found %d "
++                         "policies to update, truncated: %s",
++                         len(entries), truncated)
++
++            error = False
++
++            for entry in entries:
++                # Set unlimited BIND by default
++                entry['passwordgracelimit'] = -1
++                try:
++                    ldap.update_entry(entry)
++                except (errors.EmptyModlist, errors.NotFound):
++                    pass
++                except errors.ExecutionError as e:
++                    logger.debug("update_pwpolicy: cannot "
++                                 "update policy: %s", e)
++                    error = True
++
++            if error:
++                # Exit loop to avoid infinite cycles
++                logger.error("update_pwpolicy: error(s) "
++                             "detected during pwpolicy update")
++                return False, []
++
++            elif not truncated:
++                # All affected entries updated, exit the loop
++                logger.debug("update_pwpolicy: all policies updated")
++                return False, []
++
++        return False, []
+-- 
+2.37.2
+
diff --git a/SPECS/ipa.spec b/SPECS/ipa.spec
index d53f198..af97919 100644
--- a/SPECS/ipa.spec
+++ b/SPECS/ipa.spec
@@ -68,8 +68,7 @@
 %global krb5_kdb_version 8.0
 # 0.7.16: https://github.com/drkjam/netaddr/issues/71
 %global python_netaddr_version 0.7.19
-# Require 4.14.5-13 which brings CVE-2020-25717 fixes
-%global samba_version 4.14.5-13
+%global samba_version 4.17.2-1
 %global selinux_policy_version 3.14.3-52
 %global slapi_nis_version 0.56.4
 %global python_ldap_version 3.1.0-1
@@ -93,8 +92,7 @@
 # 0.7.16: https://github.com/drkjam/netaddr/issues/71
 %global python_netaddr_version 0.7.16
 
-# Require 4.14.6 which brings CVE-2020-25717 fixes
-%global samba_version 2:4.14.6
+%global samba_version 2:4.17.2
 
 # 3.14.5-45 or later includes a number of interfaces fixes for IPA interface
 %global selinux_policy_version 3.14.5-45
@@ -191,7 +189,7 @@
 
 Name:           %{package_name}
 Version:        %{IPA_VERSION}
-Release:        3%{?rc_version:.%rc_version}%{?dist}
+Release:        7%{?rc_version:.%rc_version}%{?dist}
 Summary:        The Identity, Policy and Audit system
 
 License:        GPLv3+
@@ -215,6 +213,12 @@ Patch0001:      0001-ipa-otpd-Fix-build-on-older-versions-of-gcc.patch
 Patch0002:      0002-webui-IdP-Remove-arrow-notation-due-to-uglify-js-lim.patch
 Patch0003:      0003-Preserve-user-fix-the-confusing-summary_rhbz#2022028.patch
 Patch0004:      0004-Only-calculate-LDAP-password-grace-when-the-password_rhbz#782917.patch
+Patch0005:      0005-Add-end-to-end-integration-tests-for-external-IdP.patch
+Patch0006:      0006-webui-Do-not-allow-empty-pagination-size_rhbz#2094672.patch
+Patch0007:      0007-webui-Allow-grace-login-limit_rhbz#2109243.patch
+Patch0008:      0008-check_repl_update-in-progress-is-a-boolean_rhbz#2117303.patch
+Patch0009:      0009-Disabling-gracelimit-does-not-prevent-LDAP-binds_rhbz#2109236.patch
+Patch0010:      0010-Set-passwordgracelimit-to-match-global-policy-on-group-pw-policies_rhbz#2115475.patch
 Patch1001:      1001-Change-branding-to-IPA-and-Identity-Management.patch
 Patch1002:      1002-Revert-freeipa.spec-depend-on-bind-dnssec-utils.patch
 %endif
@@ -1717,6 +1721,28 @@ fi
 %endif
 
 %changelog
+* Tue Nov 1  2022 Rafael Jeffman <rjeffman@redhat.com> - 4.9.10-7
+- Rebuild to samba 4.17.2.
+  Related: RHBZ#2132051
+
+* Mon Aug 22 2022 Rafael Jeffman <rjeffman@redhat.com> - 4.9.10-6
+- webui: Allow grace login limit
+  Resolves: RHBZ#2109243
+- check_repl_update: in progress is a boolean
+  Resolves: RHBZ#2117303
+- Disabling gracelimit does not prevent LDAP binds
+  Resolves: RHBZ#2109236
+- Set passwordgracelimit to match global policy on group pw policies
+  Resolves: RHBZ#2115475
+
+* Tue Jul 19 2022 Rafael Jeffman <rjeffman@redhat.com> - 4.9.10-5
+- webui: Do not allow empty pagination size
+  Resolves: RHBZ#2094672
+
+* Tue Jul 12 2022 Rafael Jeffman <rjeffman@redhat.com> - 4.9.10-4
+- Add end to end integration tests for external IdP
+  Resolves: RHBZ#2106346
+
 * Thu Jul 07 2022 Rafael Jeffman <rjeffman@redhat.com> - 4.9.10-3
 - Add explicit dependency for libvert-libev
   Resolves: RHBZ#2104929