From c8a18bb46d9fcb43e3baad1e339ef0bbc643d771 Mon Sep 17 00:00:00 2001 From: Florence Blanc-Renaud Date: Mon, 21 Oct 2024 19:24:16 +0200 Subject: [PATCH] ipa-4.12.2-2 - Related: RHEL-59788 Rebase Samba to the latest 4.21.x release - Fixes: RHEL-61642 Uninstall ACME separately during PKI uninstallation - Fixes: RHEL-56963 SSSD offline causing test-adtrust-install failure - Fixes: RHEL-56473 Include latest fixes in python3-ipatests packages - Fixes: RHEL-48104 Default hbac rules are duplicated on remote server post ipa-migrate in prod-mode - Fixes: RHEL-45330 [RFE] add a tool to quickly detect and fix issues with IPA ID ranges - Fixes: RHEL-40376 SID generation task is failing when SELinux is in Enforcing mode - Fixes: RHEL-4915 Last expired OTP token would be c Signed-off-by: Florence Blanc-Renaud --- ...-Default-PAC-type-is-added-to-config.patch | 92 + ...IPA-log-files-to-ipa_log_t-file-cont.patch | 86 + 0005-Add-ipa-idrange-fix.patch | 1501 +++++++++++++++++ ...sing-comma-in-test_idrange_no_rid_ba.patch | 36 + ...ests-Update-ipa-adtrust-install-test.patch | 32 + ...er-activate-ssh-service-in-sssd.conf.patch | 33 + ...migration-issues-with-entries-using-.patch | 413 +++++ ...te-fix-alternate-entry-search-filter.patch | 68 + ...-a-ccache-to-rpcclient-deletetrustdo.patch | 67 + ...tall-add-use-krb5-ccache-to-smbclien.patch | 60 + ...moving-the-CA-to-uninstall-the-ACME-.patch | 209 +++ ...-Fixes-for-ipa-idrange-fix-testsuite.patch | 35 + ...with-an-expired-OTP-token-to-log-in-.patch | 265 +++ 0016-ipatests-Activate-ssh-in-sssd.conf.patch | 41 + ...igrate-man-page-fix-typos-and-errors.patch | 131 ++ ...s-Test-for-ipa-hbac-rule-duplication.patch | 61 + ...r-password-file-handling-in-TestHSMI.patch | 116 ++ 0020-ipatests-2FA-test-cases.patch | 276 +++ freeipa.spec | 36 +- 19 files changed, 3555 insertions(+), 3 deletions(-) create mode 100644 0003-ipatests-Check-Default-PAC-type-is-added-to-config.patch create mode 100644 0004-selinux-add-all-IPA-log-files-to-ipa_log_t-file-cont.patch create mode 100644 0005-Add-ipa-idrange-fix.patch create mode 100644 0006-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch create mode 100644 0007-ipatests-Update-ipa-adtrust-install-test.patch create mode 100644 0008-Installer-activate-ssh-service-in-sssd.conf.patch create mode 100644 0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch create mode 100644 0010-ipa-migrate-fix-alternate-entry-search-filter.patch create mode 100644 0011-ipatests-provide-a-ccache-to-rpcclient-deletetrustdo.patch create mode 100644 0012-test_adtrust_install-add-use-krb5-ccache-to-smbclien.patch create mode 100644 0013-Don-t-rely-on-removing-the-CA-to-uninstall-the-ACME-.patch create mode 100644 0014-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch create mode 100644 0015-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch create mode 100644 0016-ipatests-Activate-ssh-in-sssd.conf.patch create mode 100644 0017-ipa-migrate-man-page-fix-typos-and-errors.patch create mode 100644 0018-ipatests-Test-for-ipa-hbac-rule-duplication.patch create mode 100644 0019-ipatests-refactor-password-file-handling-in-TestHSMI.patch create mode 100644 0020-ipatests-2FA-test-cases.patch diff --git a/0003-ipatests-Check-Default-PAC-type-is-added-to-config.patch b/0003-ipatests-Check-Default-PAC-type-is-added-to-config.patch new file mode 100644 index 0000000..517e6d1 --- /dev/null +++ b/0003-ipatests-Check-Default-PAC-type-is-added-to-config.patch @@ -0,0 +1,92 @@ +From ad4b7f6cedaed54acf279033b650010c65face10 Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Tue, 20 Aug 2024 14:52:03 +0530 +Subject: [PATCH] ipatests: Check Default PAC type is added to config + +This patch checks that the default PAC type +is added to configuration i.e ipaKrbAuthzData: MS-PAC +during ipa-server-installation + +The patch also checks that if 'ipaKrbAuthzData: MS-PAC' +attribute is deleted and then when we run 'ipa-server-upgrade' +command the attribute is added back. + +Related: https://pagure.io/freeipa/issue/9632 + +Signed-off-by: Sudhir Menon +Reviewed-By: Florence Blanc-Renaud +--- + .../test_integration/test_installation.py | 15 +++++++++++ + ipatests/test_integration/test_upgrade.py | 26 ++++++++++++++++++- + 2 files changed, 40 insertions(+), 1 deletion(-) + +diff --git a/ipatests/test_integration/test_installation.py b/ipatests/test_integration/test_installation.py +index ada43e33fe173ea3c315178c37e2a664b05b905b..c5565c452010f23f038ddf329454b591ef09f6af 100644 +--- a/ipatests/test_integration/test_installation.py ++++ b/ipatests/test_integration/test_installation.py +@@ -1190,6 +1190,21 @@ class TestInstallMaster(IntegrationTest): + expected_stdout=f'href="https://{self.master.hostname}/' + ) + ++ def test_pac_configuration_enabled(self): ++ """ ++ This testcase checks that the default PAC type ++ is added to configuration. ++ """ ++ base_dn = str(self.master.domain.basedn) ++ dn = DN( ++ ("cn", "ipaConfig"), ++ ("cn", "etc"), ++ base_dn ++ ) ++ result = tasks.ldapsearch_dm(self.master, str(dn), ++ ["ipaKrbAuthzData"]) ++ assert 'ipaKrbAuthzData: MS-PAC' in result.stdout_text ++ + def test_hostname_parameter(self, server_cleanup): + """ + Test that --hostname parameter is respected in interactive mode. +diff --git a/ipatests/test_integration/test_upgrade.py b/ipatests/test_integration/test_upgrade.py +index 011de939e92790734d63da2f85be1c25349116a8..a0f393780ccc25774466992976532c876aa876da 100644 +--- a/ipatests/test_integration/test_upgrade.py ++++ b/ipatests/test_integration/test_upgrade.py +@@ -165,7 +165,6 @@ class TestUpgrade(IntegrationTest): + ldap.update_entry(location_krb_rec) + + yield _setup_locations +- + ldap = self.master.ldap_connect() + + modified = False +@@ -491,3 +490,28 @@ class TestUpgrade(IntegrationTest): + tasks.reinstall_packages(self.master, ['*ipa-client']) + assert not self.master.transport.file_exists( + paths.SSH_CONFIG + ".orig") ++ ++ def test_mspac_attribute_set(self): ++ """ ++ This testcase deletes the already existing attribute ++ 'ipaKrbAuthzData: MS-PAC'. ++ The test then runs ipa-server-upgrade and checks that ++ the attribute 'ipaKrbAuthzData: MS-PAC' is added again. ++ """ ++ base_dn = str(self.master.domain.basedn) ++ dn = DN( ++ ("cn", "ipaConfig"), ++ ("cn", "etc"), ++ base_dn ++ ) ++ ldif = textwrap.dedent(""" ++ dn: cn=ipaConfig,cn=etc,{} ++ changetype: modify ++ delete: ipaKrbAuthzData ++ """).format(base_dn) ++ tasks.ldapmodify_dm(self.master, ldif) ++ tasks.kinit_admin(self.master) ++ self.master.run_command(['ipa-server-upgrade']) ++ result = tasks.ldapsearch_dm(self.master, str(dn), ++ ["ipaKrbAuthzData"]) ++ assert 'ipaKrbAuthzData: MS-PAC' in result.stdout_text +-- +2.46.2 + diff --git a/0004-selinux-add-all-IPA-log-files-to-ipa_log_t-file-cont.patch b/0004-selinux-add-all-IPA-log-files-to-ipa_log_t-file-cont.patch new file mode 100644 index 0000000..049b37c --- /dev/null +++ b/0004-selinux-add-all-IPA-log-files-to-ipa_log_t-file-cont.patch @@ -0,0 +1,86 @@ +From 42eb97ee6bd8011b590aef321d4386ea9352933d Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Wed, 28 Aug 2024 10:02:19 +0300 +Subject: [PATCH] selinux: add all IPA log files to ipa_log_t file context + +We have multiple log files that produced by IPA components. Some of them +are written by the tools that run as root and inherit their file context +from /var/log -> var_log_t. However, increasingly we get tools that were +run through oddjob helpers. These supposed to be run within ipa_helper_t +SELinux context which has write permissions for ipa_log_t file context. + +Add all known log files from the base platform. The following script was +used to generate them: +$ git grep '_LOG = .*ipa.*\.log' ipaplatform/base/paths.py | cut -d= -f2 | \ + xargs -I% echo -e "%\t--\tgen_context(system_u:object_r:ipa_log_t,s0)" + +/var/log/ipabackup.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaclient-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaclient-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaclientsamba-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaclientsamba-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipareplica-ca-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipareplica-conncheck.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipareplica-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/iparestore.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-enable-sid.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-adtrust-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-dns-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-kra-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaupgrade.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipatrust-enable-agent.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaepn.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipa-custodia.audit.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipa-migrate.log -- gen_context(system_u:object_r:ipa_log_t,s0) + +ipa-custodia.audit.log was already in the present list. + +Additionally, ipa-migrate-conflict.ldif is used by the ipa-migrate tool +but is not provided through the ipaplatform mechanism. It is added +explicitly. + +Fixes: https://pagure.io/freeipa/issue/9654 + +Signed-off-by: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + selinux/ipa.fc | 21 ++++++++++++++++++++- + 1 file changed, 20 insertions(+), 1 deletion(-) + +diff --git a/selinux/ipa.fc b/selinux/ipa.fc +index 700e3a14a11fcd403a2e6f57ec781c58dae77660..47bd19ba77418cad1f0904dc4a9a35ce9d6ff9d2 100644 +--- a/selinux/ipa.fc ++++ b/selinux/ipa.fc +@@ -24,7 +24,26 @@ + + /var/log/ipa(/.*)? gen_context(system_u:object_r:ipa_log_t,s0) + +-/var/log/ipareplica-conncheck.log.* -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipabackup.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaclient-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaclient-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaclientsamba-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaclientsamba-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipareplica-ca-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipareplica-conncheck.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipareplica-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/iparestore.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-enable-sid.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-adtrust-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-dns-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-kra-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaupgrade.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipatrust-enable-agent.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaepn.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipa-migrate.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipa-migrate-conflict.ldif -- gen_context(system_u:object_r:ipa_log_t,s0) + + /var/run/ipa(/.*)? gen_context(system_u:object_r:ipa_var_run_t,s0) + +-- +2.46.2 + diff --git a/0005-Add-ipa-idrange-fix.patch b/0005-Add-ipa-idrange-fix.patch new file mode 100644 index 0000000..0d291fd --- /dev/null +++ b/0005-Add-ipa-idrange-fix.patch @@ -0,0 +1,1501 @@ +From aa4651526e6697e15ce4960bf1d15d1389889c7f Mon Sep 17 00:00:00 2001 +From: "asharov@redhat.com" +Date: Mon, 24 Jun 2024 15:33:34 +0200 +Subject: [PATCH] Add ipa-idrange-fix + +ipa-idrange-fix is a tool for analysis of existing IPA ranges, users +and groups outside of those ranges, and functionality to propose +and apply remediations to make sure as much users and groups as +possible end up in the IPA-managed ranges. + +Fixes: https://pagure.io/freeipa/issue/9612 + +Signed-off-by: Aleksandr Sharov +Reviewed-By: Rob Crittenden +Reviewed-By: Rob Crittenden +--- + freeipa.spec.in | 2 + + install/tools/Makefile.am | 2 + + install/tools/ipa-idrange-fix.in | 8 + + install/tools/man/Makefile.am | 1 + + install/tools/man/ipa-idrange-fix.1 | 111 ++ + ipaserver/install/ipa_idrange_fix.py | 1085 +++++++++++++++++ + .../test_integration/test_ipa_idrange_fix.py | 189 +++ + 7 files changed, 1398 insertions(+) + create mode 100644 install/tools/ipa-idrange-fix.in + create mode 100644 install/tools/man/ipa-idrange-fix.1 + create mode 100644 ipaserver/install/ipa_idrange_fix.py + create mode 100644 ipatests/test_integration/test_ipa_idrange_fix.py + +diff --git a/freeipa.spec.in b/freeipa.spec.in +index e370290bc74d92ab239bf11e88b3fa7e4faef415..171b6ad27b57553fdd46c7d041715949bb00b163 100755 +--- a/freeipa.spec.in ++++ b/freeipa.spec.in +@@ -1517,6 +1517,7 @@ fi + %{_sbindir}/ipa-pkinit-manage + %{_sbindir}/ipa-crlgen-manage + %{_sbindir}/ipa-cert-fix ++%{_sbindir}/ipa-idrange-fix + %{_sbindir}/ipa-acme-manage + %{_sbindir}/ipa-migrate + %if 0%{?fedora} >= 38 +@@ -1596,6 +1597,7 @@ fi + %{_mandir}/man1/ipa-pkinit-manage.1* + %{_mandir}/man1/ipa-crlgen-manage.1* + %{_mandir}/man1/ipa-cert-fix.1* ++%{_mandir}/man1/ipa-idrange-fix.1* + %{_mandir}/man1/ipa-acme-manage.1* + %{_mandir}/man1/ipa-migrate.1* + +diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am +index c454fad9795c79f88e1d72688f1d15c5234cc113..ca484ec37969c9c06ae7b408b55fa30cd4e8e4fe 100644 +--- a/install/tools/Makefile.am ++++ b/install/tools/Makefile.am +@@ -31,6 +31,7 @@ dist_noinst_DATA = \ + ipa-pkinit-manage.in \ + ipa-crlgen-manage.in \ + ipa-cert-fix.in \ ++ ipa-idrange-fix.in \ + ipa-custodia.in \ + ipa-custodia-check.in \ + ipa-httpd-kdcproxy.in \ +@@ -68,6 +69,7 @@ nodist_sbin_SCRIPTS = \ + ipa-pkinit-manage \ + ipa-crlgen-manage \ + ipa-cert-fix \ ++ ipa-idrange-fix \ + ipa-acme-manage \ + ipa-migrate \ + $(NULL) +diff --git a/install/tools/ipa-idrange-fix.in b/install/tools/ipa-idrange-fix.in +new file mode 100644 +index 0000000000000000000000000000000000000000..5994bd28b15e247c5a086238f36b16cc75ff24c3 +--- /dev/null ++++ b/install/tools/ipa-idrange-fix.in +@@ -0,0 +1,8 @@ ++#!/usr/bin/python3 ++# ++# Copyright (C) 2024 FreeIPA Contributors see COPYING for license ++# ++ ++from ipaserver.install.ipa_idrange_fix import IPAIDRangeFix ++ ++IPAIDRangeFix.run_cli() +diff --git a/install/tools/man/Makefile.am b/install/tools/man/Makefile.am +index 34f359863afca7b6c1e792a53afc25bb8eb41fd3..e9542a77bbbb88054eae1e64311d6e9ec5bee499 100644 +--- a/install/tools/man/Makefile.am ++++ b/install/tools/man/Makefile.am +@@ -29,6 +29,7 @@ dist_man1_MANS = \ + ipa-pkinit-manage.1 \ + ipa-crlgen-manage.1 \ + ipa-cert-fix.1 \ ++ ipa-idrange-fix.1 \ + ipa-acme-manage.1 \ + ipa-migrate.1 \ + $(NULL) +diff --git a/install/tools/man/ipa-idrange-fix.1 b/install/tools/man/ipa-idrange-fix.1 +new file mode 100644 +index 0000000000000000000000000000000000000000..178d2e88779e135a65f3285de62d2dc3b19c175a +--- /dev/null ++++ b/install/tools/man/ipa-idrange-fix.1 +@@ -0,0 +1,111 @@ ++.\" ++.\" Copyright (C) 2024 FreeIPA Contributors see COPYING for license ++.\" ++.TH "ipa-idrange-fix" "1" "May 26 2024" "IPA" "IPA Manual Pages" ++.SH "NAME" ++ipa\-idrange\-fix \- Analyse and fix IPA ID ranges ++.SH "SYNOPSIS" ++ipa\-idrange\-fix [options] ++.SH "DESCRIPTION" ++ ++\fIipa-idrange-fix\fR is a tool for analysis of existing IPA ranges, users and ++groups outside of those ranges, and functionality to propose and apply ++remediations to make sure as many users and groups as possible end up in the ++IPA-managed ranges. Before any changes are applied, a full backup of the system ++is \fBSTRONGLY RECOMMENDED\fR. ++ ++Do not use this program in unattended mode unless you are absolutely sure ++you are consenting to the tool's proposals. ++ ++You can apply the proposals manually via \fIipa idrange(1)\fR commands. ++ ++This tool requires it to be run as \fBroot\fR and does not require a kerberos ++ticket. The directory server needs to be running. ++ ++\fIipa-idrange-fix\fR will read current ranges from LDAP, then check their ++basic constraints, RID bases, etc. If it finds critical issues with ranges, ++manual adjustment will be required. ++ ++After analyzing existing ranges, the tool will search for users and groups that ++are outside of ipa-local ranges. Then it will attempt to propose new ipa-local ++ranges in order to cover users and groups found. ++ ++Finally, the tool will summarize the analysis, and, if there are proposed ++changes, will ask if the user wants to apply those. Please read the ++proposals carefully before proceeding with changes! ++ ++Important note: By default, \fIipa-idrange-fix\fR will not cover the users and ++groups that have IDs under 1000 as these IDs are reserved for system and ++service users and groups. We \fBdon't recommend\fR using IDs under 1000 for ++IPA users and groups as they can possibly overlap with local ones. Please ++consider moving those users out of the range 1..1000, unless they are ++absolutely needed. ++ ++.SH "OPTIONS" ++.TP ++\fB\-\-version\fR ++Show the program's version and exit. ++.TP ++\fB\-h\fR, \fB\-\-help\fR ++Show the help for this program. ++.TP ++\fB\-\-ridoffset \fIINT\fR ++An offset for newly proposed base RIDs for ranges. We introduce offset in order ++to have an ability to increase ranges in the future, increase to more than ++offset will result in RID bases overlapping, and will be denied. If set to 0, ++there will be no offset, proposed RID ranges will start directly one after ++another. ++ ++Default - \fI100000\fR, allowed values - from \fI0\fR to \fI2^31\fR. ++.TP ++\fB\-\-rangegap \fIINT\fR ++A number of IDs between out of ranges IDs to be considered too big to be inside ++a proposed range. If the gap is bigger than this attribute, a new range will be ++started. If set to 0, every entity will get its own range, if allowed by ++\fI--minrange\fR. ++ ++Default - \fI200000\fR, allowed values - from \fI0\fR to \fI2^31\fR. ++.TP ++\fB\-\-minrange \fIINT\fR ++A minimal amount of IDs the tool considers to be a valid range. All IDs that ++would form a range with less than this number will be considered outliers, not ++worth creating an IDrange for, and will be listed explicitly to be moved ++manually. If set to 1, a range will be proposed for every entity, even if the ++entity is single in the middle of an empty space. ++ ++Default - \fI10\fR, allowed values - from \fI1\fR to \fI2^31\fR. ++.TP ++\fB\-\-allowunder1000\fR ++A flag to allow proposing ranges that start with IDs lower than \fI1000\fR. ++Remember, this is not recommended - IDs under 1000 are reserved for system and ++service users and groups. IDranges with these low IDs may result with ++overlapping of IPA and system local users and groups, which can be a serious ++security issue and generally produce a lot of issues around these entities' ++resolution. ++.TP ++\fB\-\-norounding\fR ++A flag to turn off idrange starting id and size rounding - e.g. if we find ++ID 1234, and the size 567, it will stay that way, the proposed range will ++start at ID 1234, and have a 567 size. If not specified, basic rounding to ++outer margins will be applied. Rounding will be 10^size of the proposed range. ++.TP ++\fB\-\-unattended\fR ++Run the tool in unattended mode, if any changes would be proposed, they will ++be applied automatically. ++.TP ++\fB\-v\fR, \fB\-\-verbose\fR ++Print debugging information. ++.TP ++\fB\-q\fR, \fB\-\-quiet\fR ++Output only errors (output from child processes may still be shown). ++.TP ++\fB\-\-log\-file\fR=\fIFILE\fR ++Log to the given file. ++.SH "EXIT STATUS" ++0 if the command was successful ++ ++1 if an error occurred ++ ++.SH "SEE ALSO" ++.BR ipa\ idrange-mod(1) ++.BR ipa\ idrange-add(1) +diff --git a/ipaserver/install/ipa_idrange_fix.py b/ipaserver/install/ipa_idrange_fix.py +new file mode 100644 +index 0000000000000000000000000000000000000000..c6c67ae9330e2d0184efc09d09a84216ef0772a6 +--- /dev/null ++++ b/ipaserver/install/ipa_idrange_fix.py +@@ -0,0 +1,1085 @@ ++"""Tool to analyze and fix IPA ID ranges""" ++# ++# Copyright (C) 2024 FreeIPA Contributors see COPYING for license ++# ++ ++import logging ++import ldap ++ ++from ipalib import api, errors ++from ipapython.admintool import AdminTool ++from ipapython.dn import DN ++from ipapython import ipautil ++from typing import List, Tuple ++ ++logger = logging.getLogger(__name__) ++ ++ ++class IDRange: ++ """Class for ID range entity""" ++ ++ def __init__(self): ++ self.last_id: int = None ++ self.last_base_rid: int = None ++ self.last_secondary_rid: int = None ++ self.name: str = None ++ self.size: int = None ++ self.first_id: int = None ++ self.base_rid: int = None ++ self.secondary_base_rid: int = None ++ self.type: str = None ++ self.suffix: str = None ++ self.dn: str = None ++ ++ def _count(self) -> None: ++ """Function to calculate last IDs for the range""" ++ self.last_id = self.first_id + self.size - 1 ++ if self.type == "ipa-local": ++ self.last_base_rid = ( ++ self.base_rid + self.size ++ if self.base_rid is not None ++ else None ++ ) ++ self.last_secondary_rid = ( ++ self.secondary_base_rid + self.size ++ if self.secondary_base_rid is not None ++ else None ++ ) ++ ++ def __repr__(self): ++ return ( ++ f"IDRange(name='{self.name}', " ++ f"type={self.type}, " ++ f"size={self.size}, " ++ f"first_id={self.first_id}, " ++ f"base_rid={self.base_rid}, " ++ f"secondary_base_rid={self.secondary_base_rid})" ++ ) ++ ++ def __eq__(self, other): ++ return self.first_id == other.first_id ++ ++ ++class IDentity: ++ """A generic class for ID entity - users or groups""" ++ ++ def __init__(self, **kwargs): ++ self.dn: str = kwargs.get('dn') ++ self.name: str = kwargs.get('name') ++ self.user: str = kwargs.get('user') ++ self.number: int = kwargs.get('number') ++ ++ def __str__(self): ++ if self.user: ++ return (f"user '{self.name}', uid={self.number}") ++ return (f"group '{self.name}', gid={self.number}") ++ ++ def debug(self): ++ if self.user: ++ return ( ++ f"user(username='{self.name}', " ++ f"uid={self.number}, " ++ f"{self.dn})" ++ ) ++ return ( ++ f"group(groupname='{self.name}', " ++ f"gid={self.number}, " ++ f"{self.dn})" ++ ) ++ ++ def __eq__(self, other): ++ return self.number == other.number and self.user == other.user ++ ++ ++class IPAIDRangeFix(AdminTool): ++ """Tool to analyze and fix IPA ID ranges""" ++ ++ command_name = "ipa-idrange-fix" ++ log_file_name = "/var/log/ipa-idrange-fix.log" ++ usage = "%prog" ++ description = "Analyze and fix IPA ID ranges" ++ ++ @classmethod ++ def add_options(cls, parser, debug_option=False): ++ super(IPAIDRangeFix, cls).add_options(parser) ++ parser.add_option( ++ "--ridoffset", ++ dest="ridoffset", ++ type=int, ++ default=100000, ++ metavar=100000, ++ help="Offset for a next base RID from previous RID range. \ ++Needed for future range size expansions. Has to be > 0", ++ ) ++ parser.add_option( ++ "--rangegap", ++ dest="rangegap", ++ type=int, ++ default=200000, ++ metavar=200000, ++ help="Threshold for a gap between out-of-range IDs to be \ ++considered a different range. Has to be > 0", ++ ) ++ parser.add_option( ++ "--minrange", ++ dest="minrange", ++ type=int, ++ default=10, ++ metavar=10, ++ help="Minimal considered range size for out-of-range IDs.\ ++All ranges with amount of IDs lower than this number will be discarded and \ ++IDs will be listed to be moved manually. Has to be > 1", ++ ) ++ parser.add_option( ++ "--allowunder1000", ++ dest="allowunder1000", ++ action="store_true", ++ default=False, ++ help="Allow idranges to start below 1000. Be careful to not \ ++overlap IPA users/groups with existing system-local ones!", ++ ) ++ parser.add_option( ++ "--norounding", ++ dest="norounding", ++ action="store_true", ++ default=False, ++ help="Disable IDrange rounding attempt in order to get ranges \ ++exactly covering just IDs provided", ++ ) ++ parser.add_option( ++ "--unattended", ++ dest="unattended", ++ action="store_true", ++ default=False, ++ help="Automatically fix all range issues found without asking \ ++for confirmation", ++ ) ++ ++ def __init__(self, *args, **kwargs): ++ super().__init__(*args, **kwargs) ++ self.realm: str = None ++ self.suffix: DN = None ++ self.proposals_rid: List[IDRange] = [] ++ self.proposals_new: List[IDRange] = [] ++ self.outliers: List[IDentity] = [] ++ self.under1000: List[IDentity] = [] ++ self.id_ranges: List[IDRange] = [] ++ ++ def validate_options(self, needs_root=True): ++ super().validate_options(needs_root) ++ ++ def run(self): ++ api.bootstrap(in_server=True) ++ api.finalize() ++ ++ self.realm = api.env.realm ++ self.suffix = ipautil.realm_to_suffix(self.realm) ++ try: ++ api.Backend.ldap2.connect() ++ ++ # Reading range data ++ self.id_ranges = read_ranges(self.suffix) ++ ++ # Evaluating existing ranges, if something is off, exit ++ if self.evaluate_ranges() != 0: ++ return 1 ++ ++ # reading out of range IDs ++ ids_out_of_range = read_outofrange_identities( ++ self.suffix, self.id_ranges ++ ) ++ ++ # Evaluating out of range IDs ++ self.evaluate_identities(ids_out_of_range) ++ ++ # Print the proposals ++ self.print_intentions() ++ ++ # If there are no proposals, we have nothing to do, exiting ++ if (len(self.proposals_rid) == 0 ++ and len(self.proposals_new) == 0): ++ logger.info("\nNo changes proposed, nothing to do.") ++ return 0 ++ ++ logger.info("\nID ranges table after proposed changes:") ++ draw_ascii_table(self.id_ranges) ++ ++ if self.options.unattended: ++ logger.info( ++ "Unattended mode, proceeding with applying changes!" ++ ) ++ else: ++ response = ipautil.user_input('Enter "yes" to proceed') ++ if response.lower() != "yes": ++ logger.info("Not proceeding.") ++ return 0 ++ logger.info("Proceeding.") ++ ++ # Applying changes ++ for id_range in self.proposals_rid: ++ apply_ridbases(id_range) ++ ++ for id_range in self.proposals_new: ++ create_range(id_range) ++ ++ logger.info("All changes applied successfully!") ++ ++ finally: ++ if api.Backend.ldap2.isconnected(): ++ api.Backend.ldap2.disconnect() ++ ++ return 0 ++ ++ def evaluate_ranges(self) -> int: ++ """Function to evaluate existing ID ranges""" ++ if len(self.id_ranges) == 0: ++ logger.error("No ID ranges found!") ++ return 1 ++ ++ draw_ascii_table(self.id_ranges) ++ ++ if not ranges_overlap_check(self.id_ranges): ++ logger.error( ++ "Ranges overlap detected, cannot proceed! Please adjust \ ++existing ranges manually." ++ ) ++ return 1 ++ ++ # Checking RID bases for existing ranges ++ id_ranges_nobase = get_ranges_no_base(self.id_ranges) ++ ++ if len(id_ranges_nobase) > 0: ++ logger.info( ++ "Found %s ranges without base RIDs", len(id_ranges_nobase) ++ ) ++ for id_range in id_ranges_nobase: ++ logger.debug( ++ "Range '%s' has RID base %s and secondary RID base %s", ++ id_range.name, ++ id_range.base_rid, ++ id_range.secondary_base_rid, ++ ) ++ propose_rid_ranges( ++ self.id_ranges, ++ self.options.ridoffset, ++ self.proposals_rid ++ ) ++ else: ++ logger.info( ++ "All ID ranges have base RIDs set, RID adjustments are \ ++not needed." ++ ) ++ return 0 ++ ++ def evaluate_identities(self, ids_out_of_range: List[IDentity]) -> None: ++ """Function to evaluate out of range IDs""" ++ if len(ids_out_of_range) == 0: ++ logger.info("No out of range IDs found!") ++ else: ++ logger.info( ++ "Found overall %s IDs out of existing ID ranges.\n", ++ len(ids_out_of_range), ++ ) ++ # ruling out IDs under 1000 if flag is not set ++ if not self.options.allowunder1000: ++ self.under1000, ids_out_of_range = separate_under1000( ++ ids_out_of_range ++ ) ++ if len(self.under1000) > 0: ++ logger.info( ++ "Found IDs under 1000, which is not recommeneded \ ++(if you definitely need ranges proposed for those, use --allowunder1000):" ++ ) ++ for identity in self.under1000: ++ logger.info("%s", identity) ++ ++ # Get initial divide of IDs into groups ++ groups = group_identities_by_threshold( ++ ids_out_of_range, self.options.rangegap ++ ) ++ ++ # Get outliers from too small groups and clean groups for ++ # further processing ++ self.outliers, cleangroups = separate_ranges_and_outliers( ++ groups, self.options.minrange ++ ) ++ ++ # Print the outliers, they have to be moved manually ++ if len(self.outliers) > 0: ++ logger.info( ++ "\nIdentities that don't fit the criteria to get a new " ++ "range found! Current attributes:\n" ++ "Minimal range size: %s\n" ++ "Maximum gap between IDs: %s\n" ++ "Try adjusting --minrange, --rangegap or move the " ++ "following identities into already existing ranges:", ++ self.options.minrange, ++ self.options.rangegap ++ ) ++ for identity in self.outliers: ++ logger.info("%s", identity) ++ ++ if len(cleangroups) > 0: ++ # Get IDrange name base ++ basename = get_rangename_base(self.id_ranges) ++ ++ # Create proposals for new ranges from groups ++ for group in cleangroups: ++ newrange = propose_range( ++ group, ++ self.id_ranges, ++ self.options.ridoffset, ++ basename, ++ self.options.norounding, ++ self.options.allowunder1000 ++ ) ++ if newrange is not None: ++ self.proposals_new.append(newrange) ++ self.id_ranges.append(newrange) ++ self.id_ranges.sort(key=lambda x: x.first_id) ++ else: ++ logger.info( ++ "\nNo IDs fit the criteria for a new ID range to propose!" ++ ) ++ ++ def print_intentions(self) -> None: ++ """Function to print out the summary of the proposed changes""" ++ logger.info("\nSummary:") ++ ++ if len(self.outliers) > 0: ++ logger.info("Outlier IDs that are too far away to get a range:") ++ for identity in self.outliers: ++ logger.info("%s", identity) ++ ++ if len(self.under1000) > 0: ++ if self.options.allowunder1000: ++ logger.info("IDs under 1000 were treated like normal IDs.") ++ else: ++ logger.info("IDs under 1000:") ++ for identity in self.under1000: ++ logger.info("%s", identity) ++ else: ++ logger.info("No IDs under 1000 found.") ++ ++ if len(self.proposals_rid) > 0: ++ logger.info("Proposed changes to existing ranges:") ++ for id_range in self.proposals_rid: ++ logger.info( ++ "Range '%s' - base RID: %s, secondary base RID: %s", ++ id_range.name, ++ id_range.base_rid, ++ id_range.secondary_base_rid, ++ ) ++ else: ++ logger.info("No changes proposed for existing ranges.") ++ ++ if len(self.proposals_new) > 0: ++ logger.info("Proposed new ranges:") ++ for id_range in self.proposals_new: ++ logger.info("%s", id_range) ++ else: ++ logger.info("No new ranges proposed.") ++ ++# Working with output ++# region ++ ++ ++def draw_ascii_table(id_ranges: List[IDRange], stdout: bool = False) -> None: ++ """Function to draw a table with ID ranges in ASCII""" ++ table: str = "\n" ++ # Calculate the maximum width required for each column using column names ++ max_widths = { ++ column: max( ++ len(str(column)), ++ max( ++ ( ++ len(str(getattr(id_range, column))) ++ if getattr(id_range, column) is not None ++ else 0 ++ ) ++ for id_range in id_ranges ++ ), ++ ) ++ for column in [ ++ "name", ++ "type", ++ "size", ++ "first_id", ++ "last_id", ++ "base_rid", ++ "last_base_rid", ++ "secondary_base_rid", ++ "last_secondary_rid", ++ ] ++ } ++ ++ # Draw the table header ++ header = "| " ++ for column, width in max_widths.items(): ++ header += f"{column.ljust(width)} | " ++ horizontal_line = "-" * (len(header) - 1) ++ table += horizontal_line + "\n" ++ table += header + "\n" ++ table += horizontal_line + "\n" ++ ++ # Draw the table rows ++ for id_range in id_ranges: ++ row = "| " ++ for column, width in max_widths.items(): ++ value = getattr(id_range, column) ++ if value is not None: ++ row += f"{str(value).rjust(width)} | " ++ else: ++ # Adding the separator ++ row += " " * (width + 1) + "| " ++ table += row + "\n" ++ table += horizontal_line + "\n" ++ if stdout: ++ print(table) ++ else: ++ logger.info(table) ++# endregion ++# Reading from LDAP ++# region ++ ++ ++def read_ranges(suffix) -> List[IDRange]: ++ """Function to read ID ranges from LDAP""" ++ id_ranges: IDRange = [] ++ try: ++ ranges = api.Backend.ldap2.get_entries( ++ DN(api.env.container_ranges, suffix), ++ ldap.SCOPE_ONELEVEL, ++ "(objectclass=ipaIDRange)", ++ ) ++ except errors.NotFound: ++ logger.error("LDAPError: No ranges found!") ++ except errors.ExecutionError as e: ++ logger.error("Exception while reading users: %s", e) ++ else: ++ for entry in ranges: ++ sv = entry.single_value ++ id_range = IDRange() ++ id_range.name = sv.get("cn") ++ id_range.size = int(sv.get("ipaidrangesize")) ++ id_range.first_id = int(sv.get("ipabaseid")) ++ id_range.base_rid = ( ++ int(sv.get("ipabaserid")) if sv.get("ipabaserid") else None ++ ) ++ id_range.secondary_base_rid = ( ++ int(sv.get("ipasecondarybaserid")) ++ if sv.get("ipasecondarybaserid") ++ else None ++ ) ++ id_range.suffix = suffix ++ id_range.type = sv.get("iparangetype") ++ id_range.dn = entry.dn ++ ++ id_range._count() ++ logger.debug("ID range found: %s", id_range) ++ ++ id_ranges.append(id_range) ++ ++ id_ranges.sort(key=lambda x: x.first_id) ++ return id_ranges ++ ++ ++def read_outofrange_identities(suffix, id_ranges) -> List[IDentity]: ++ """Function to read out of range users and groups from LDAP""" ++ users_outofrange = read_ldap_ids( ++ DN(api.env.container_user, suffix), ++ True, ++ id_ranges ++ ) ++ logger.info("Users out of range found: %s", len(users_outofrange)) ++ del_outofrange = read_ldap_ids( ++ DN(api.env.container_deleteuser, suffix), ++ True, ++ id_ranges ++ ) ++ logger.info("Preserved users out of range found: %s", len(del_outofrange)) ++ groups_outofrange = read_ldap_ids( ++ DN(api.env.container_group, suffix), ++ False, ++ id_ranges ++ ) ++ logger.info("Groups out of range found: %s", len(groups_outofrange)) ++ outofrange = users_outofrange + del_outofrange + groups_outofrange ++ outofrange.sort(key=lambda x: x.number) ++ return outofrange ++ ++ ++def read_ldap_ids(container_dn, user: bool, id_ranges) -> List[IDentity]: ++ """Function to read IDs from containter in LDAP""" ++ id_entities = [] ++ if user: ++ id_name = "user" ++ ldap_filter = get_outofrange_filter( ++ id_ranges, ++ "posixaccount", ++ "uidNumber" ++ ) ++ else: ++ id_name = "group" ++ ldap_filter = get_outofrange_filter( ++ id_ranges, ++ "posixgroup", ++ "gidNumber" ++ ) ++ ++ logger.debug("Searching %ss in %s with filter: %s", id_name, container_dn, ++ ldap_filter) ++ try: ++ identities = api.Backend.ldap2.get_entries( ++ container_dn, ++ ldap.SCOPE_ONELEVEL, ++ ldap_filter, ++ ) ++ for entry in identities: ++ id_entities.append(read_identity(entry, user)) ++ except errors.NotFound: ++ logger.debug("No out of range %ss found in %s!", id_name, container_dn) ++ except errors.ExecutionError as e: ++ logger.error("Exception while reading %s: %s", container_dn, e) ++ return id_entities ++ ++ ++def read_identity(ldapentry, user: bool = True) -> IDentity: ++ """Function to convert LDAP entry to IDentity object""" ++ sv = ldapentry.single_value ++ id_entity = IDentity() ++ id_entity.dn = ldapentry.dn ++ id_entity.name = sv.get("cn") ++ id_entity.number = ( ++ int(sv.get("uidNumber")) if user else int(sv.get("gidNumber")) ++ ) ++ id_entity.user = user ++ logger.debug("Out of range found: %s", id_entity.debug()) ++ return id_entity ++ ++ ++def get_outofrange_filter( ++ id_ranges_all: List[IDRange], object_class: str, posix_id: str ++) -> str: ++ """Function to create LDAP filter for out of range users and groups""" ++ # we need to look only for ipa-local ranges ++ id_ranges = get_ipa_local_ranges(id_ranges_all) ++ ++ ldap_filter = f"(&(objectClass={object_class})(|" ++ ++ # adding gaps in ranges to the filter ++ for i in range(len(id_ranges) + 1): ++ if i == 0: ++ start_condition = f"({posix_id}>=1)" ++ else: ++ start_condition = f"({posix_id}>={id_ranges[i - 1].last_id + 1})" ++ ++ if i < len(id_ranges): ++ end_condition = f"({posix_id}<={id_ranges[i].first_id - 1})" ++ else: ++ end_condition = f"({posix_id}<=2147483647)" ++ ++ ldap_filter += f"(&{start_condition}{end_condition})" ++ ++ ldap_filter += "))" ++ ++ return ldap_filter ++# endregion ++# Writing to LDAP ++# region ++ ++ ++def apply_ridbases(id_range: IDRange) -> None: ++ """Funtion to apply RID bases to the range in LDAP""" ++ try: ++ api.Backend.ldap2.modify_s( ++ id_range.dn, ++ [ ++ (ldap.MOD_ADD, "ipaBaseRID", str(id_range.base_rid)), ++ ( ++ ldap.MOD_ADD, ++ "ipaSecondaryBaseRID", ++ str(id_range.secondary_base_rid), ++ ), ++ ], ++ ) ++ logger.info("RID bases updated for range '%s'", id_range.name) ++ ++ except ldap.CONSTRAINT_VIOLATION as e: ++ logger.error( ++ "Failed to add RID bases to the range '%s': %s", ++ id_range.name, ++ e ++ ) ++ raise RuntimeError("Constraint violation.\n") from e ++ ++ except Exception as e: ++ logger.error( ++ "Exception while updating RID bases for range '%s': %s", ++ id_range.name, ++ e, ++ ) ++ raise RuntimeError("Failed to update RID bases.\n") from e ++ ++ ++def create_range(id_range: IDRange) -> None: ++ """Function to create a new range in LDAP""" ++ try: ++ logger.info("Creating range '%s'...", id_range.name) ++ ++ entry = api.Backend.ldap2.make_entry( ++ DN(id_range.dn), ++ objectclass=["ipaIDRange", "ipaDomainIDRange"], ++ ipaidrangesize=[str(id_range.size)], ++ ipabaseid=[str(id_range.first_id)], ++ ipabaserid=[str(id_range.base_rid)], ++ ipasecondarybaserid=[str(id_range.secondary_base_rid)], ++ iparangetype=[id_range.type], ++ ) ++ ++ api.Backend.ldap2.add_entry(entry) ++ logger.info("Range '%s' created successfully", id_range.name) ++ except Exception as e: ++ logger.error( ++ "Exception while creating range '%s': %s", ++ id_range.name, ++ e ++ ) ++ raise RuntimeError("Failed to create range.\n") from e ++# endregion ++# Working with ranges ++# region ++ ++ ++def get_ipa_local_ranges(id_ranges: List[IDRange]) -> List[IDRange]: ++ """Function to get only ipa-local ranges from the list of ranges""" ++ ipa_local_ranges = [] ++ ++ for id_range in id_ranges: ++ if id_range.type == "ipa-local": ++ ipa_local_ranges.append(id_range) ++ ++ return ipa_local_ranges ++ ++ ++def range_overlap_check( ++ range1_start: int, range1_end: int, range2_start: int, range2_end: int ++) -> bool: ++ """Function to check if two ranges overlap""" ++ # False when overlapping ++ return not (range1_start <= range2_end and range2_start <= range1_end) ++ ++ ++def range_overlap_check_idrange(range1: IDRange, range2: IDRange) -> bool: ++ """Function to check if two ranges overlap""" ++ # False when overlapping ++ return range_overlap_check( ++ range1.first_id, range1.last_id, range2.first_id, range2.last_id) ++ ++ ++def newrange_overlap_check( ++ id_ranges: List[IDRange], newrange: IDRange ++) -> bool: ++ """Function to check if proposed range overlaps with existing ones""" ++ for id_range in id_ranges: ++ if not range_overlap_check_idrange(id_range, newrange): ++ return False ++ return True ++ ++ ++def ranges_overlap_check(id_ranges: List[IDRange]) -> bool: ++ """Function to check if any of the existing ranges overlap""" ++ if len(id_ranges) < 2: ++ return True ++ for i in range(len(id_ranges) - 1): ++ for j in range(i + 1, len(id_ranges)): ++ if not range_overlap_check_idrange(id_ranges[i], id_ranges[j]): ++ logger.error( ++ "Ranges '%s' and '%s' overlap!", ++ id_ranges[i].name, ++ id_ranges[j].name, ++ ) ++ return False ++ return True ++# endregion ++# Working with RID bases ++# region ++ ++ ++def propose_rid_ranges( ++ id_ranges: List[IDRange], delta: int, proposals: List[IDRange] ++) -> None: ++ """ ++ Function to propose RID bases for ranges that don't have them set. ++ ++ - delta represents how far we start new base off existing range, ++ used in order to allow for future expansion of existing ranges up ++ to [delta] IDs. ++ """ ++ ipa_local_ranges = get_ipa_local_ranges(id_ranges) ++ ++ for id_range in ipa_local_ranges: ++ proposed_base_rid = 0 ++ proposed_secondary_base_rid = 0 ++ ++ # Calculate proposed base RID and secondary base RID ++ if id_range.base_rid is None: ++ result, proposed_base_rid = propose_rid_base( ++ id_range, ipa_local_ranges, delta, True ++ ) ++ if result: ++ id_range.base_rid = proposed_base_rid ++ id_range.last_base_rid = proposed_base_rid + id_range.size ++ else: ++ # if this fails too, we print the warning and abandon the idea ++ logger.warning( ++ "Warning: Proposed base RIDs %s for '%s' both failed, \ ++please adjust manually", ++ proposed_base_rid, ++ id_range.name, ++ ) ++ continue ++ ++ if id_range.secondary_base_rid is None: ++ result, proposed_secondary_base_rid = propose_rid_base( ++ id_range, ipa_local_ranges, delta, False, proposed_base_rid ++ ) ++ if result: ++ id_range.secondary_base_rid = proposed_secondary_base_rid ++ id_range.last_secondary_rid = ( ++ proposed_secondary_base_rid + id_range.size ++ ) ++ else: ++ # if this fails too, we print the warning and abandon the idea ++ logger.warning( ++ "Warning: Proposed secondary base RIDs %s for '%s' \ ++both failed, please adjust manually", ++ proposed_secondary_base_rid, ++ id_range.name, ++ ) ++ continue ++ ++ # Add range to the proposals if we changed something successfully ++ if proposed_base_rid > 0 or proposed_secondary_base_rid > 0: ++ logger.debug( ++ "Proposed RIDs for range '%s': pri %s, sec %s", ++ id_range.name, ++ proposed_base_rid, ++ proposed_secondary_base_rid, ++ ) ++ proposals.append(id_range) ++ ++ ++def propose_rid_base( ++ idrange: IDRange, ++ ipa_local_ranges: List[IDRange], ++ delta: int, ++ primary: bool = True, ++ previous_base_rid: int = -1 ++) -> Tuple[bool, str]: ++ """ ++ Function to propose a base RID for a range, primary or secondary. ++ We are getting the biggest base RID + size + delta and try ++ if it's a viable option, check same kind first, then the other. ++ """ ++ proposed_base_rid = max_rid(ipa_local_ranges, primary) + delta ++ if proposed_base_rid == previous_base_rid: ++ proposed_base_rid += idrange.size + delta ++ if check_rid_base(ipa_local_ranges, proposed_base_rid, idrange.size): ++ return True, proposed_base_rid ++ ++ # if we fail, we try the same with biggest of a different kind ++ proposed_base_rid_orig = proposed_base_rid ++ proposed_base_rid = max_rid(ipa_local_ranges, not primary) + delta ++ if proposed_base_rid == previous_base_rid: ++ proposed_base_rid += idrange.size + delta ++ if check_rid_base(ipa_local_ranges, proposed_base_rid, idrange.size): ++ return True, proposed_base_rid ++ ++ # if it fails, we return both RID proposals for the range ++ return False, f"{proposed_base_rid_orig} and {proposed_base_rid}" ++ ++ ++def max_rid(id_ranges: List[IDRange], primary: bool = True) -> int: ++ """Function to get maximum RID of primary or secondary RIDs""" ++ maximum_rid = 0 ++ for id_range in id_ranges: ++ ++ # looking only for primary RIDs ++ if primary: ++ if id_range.last_base_rid is not None: ++ maximum_rid = max(maximum_rid, id_range.last_base_rid) ++ # looking only for secondary RIDs ++ else: ++ if id_range.last_secondary_rid is not None: ++ maximum_rid = max(maximum_rid, id_range.last_secondary_rid) ++ ++ return maximum_rid ++ ++ ++def check_rid_base(id_ranges: List[IDRange], base: int, size: int) -> bool: ++ """Function to check if proposed RID base is viable""" ++ end = base + size + 1 ++ ++ # Checking sanity of RID range ++ if base + size > 2147483647: ++ return False ++ if base < 1000: ++ return False ++ ++ # Checking RID range overlaps ++ for id_range in id_ranges: ++ # we are interested only in ipa-local ranges ++ if id_range.type != "ipa-local": ++ continue ++ ++ # if there is no base rid set, there is no secondary base rid set, ++ # so nothing to overlap with ++ if id_range.base_rid is None: ++ continue ++ ++ # checking for an overlap ++ if not range_overlap_check( ++ base, end, id_range.base_rid, id_range.last_base_rid ++ ): ++ logger.debug( ++ "RID check failure: proposed Primary %s + %s, \ ++intersects with %s-%s from range '%s'", ++ base, ++ size, ++ id_range.base_rid, ++ id_range.last_base_rid, ++ id_range.name, ++ ) ++ return False ++ ++ # if there is no secondary base rid set, nothing to overlap with ++ if id_range.secondary_base_rid is None: ++ continue ++ ++ # if either start of end of the range fails inside existing range, ++ # or existing range is inside proposed one, we have an overlap ++ if not range_overlap_check( ++ base, end, id_range.secondary_base_rid, id_range.last_secondary_rid ++ ): ++ logger.debug( ++ "RID check failure: proposed Secondary %s + %s, \ ++intersects with %s-%s from range '%s'", ++ base, ++ size, ++ id_range.secondary_base_rid, ++ id_range.last_secondary_rid, ++ id_range.name, ++ ) ++ return False ++ ++ return True ++ ++ ++def get_ranges_no_base(id_ranges: List[IDRange]) -> List[IDRange]: ++ """Function to get ranges without either of base RIDs set""" ++ ipa_local_ranges = get_ipa_local_ranges(id_ranges) ++ ranges_no_base = [] ++ for id_range in ipa_local_ranges: ++ if id_range.base_rid is None or id_range.secondary_base_rid is None: ++ ranges_no_base.append(id_range) ++ ++ return ranges_no_base ++# endregion ++# Working with IDentities out of range ++# region ++ ++ ++def group_identities_by_threshold( ++ identities: List[IDentity], threshold: int ++) -> List[List[IDentity]]: ++ """Function to group out of range IDs by threshold""" ++ groups: List[List[IDentity]] = [] ++ currentgroup: List[IDentity] = [] ++ if len(identities) == 0: ++ return groups ++ ++ for i in range(len(identities) - 1): ++ # add id to current group ++ currentgroup.append(identities[i]) ++ ++ # If the difference with the next one is greater than the threshold, ++ # start a new group ++ if identities[i + 1].number - identities[i].number > threshold: ++ groups.append(currentgroup) ++ currentgroup = [] ++ ++ # Add the last ID number to the last group ++ currentgroup.append(identities[-1]) ++ groups.append(currentgroup) ++ ++ return groups ++ ++ ++def separate_under1000( ++ identities: List[IDentity], ++) -> Tuple[List[IDentity], List[IDentity]]: ++ """Function to separate IDs under 1000, expects sorted list""" ++ for i, identity in enumerate(identities): ++ if identity.number >= 1000: ++ return identities[:i], identities[i:] ++ return identities, [] ++ ++ ++def separate_ranges_and_outliers( ++ groups: List[List[IDentity]], minrangesize=int ++) -> Tuple[List[List[IDentity]], List[List[IDentity]]]: ++ """Function to separate IDs into outliers and IDs that can get ranges""" ++ outliers = [] ++ cleangroups = [] ++ for group in groups: ++ # if group is smaller than minrangesize, add it's memebers to ourliers ++ if group[-1].number - group[0].number + 1 < minrangesize: ++ for identity in group: ++ outliers.append(identity) ++ # if the group is OK, add it to cleaned groups ++ else: ++ cleangroups.append(group) ++ ++ return outliers, cleangroups ++ ++ ++def round_idrange(start: int, end: int, under1000: bool) -> Tuple[int, int]: ++ """Function to round up range margins to look pretty""" ++ # calculating power of the size ++ sizepower = len(str(end - start + 1)) ++ # multiplier for the nearest rounded number ++ multiplier = 10 ** (sizepower - 1) ++ # getting rounded range margins ++ rounded_start = (start // multiplier) * multiplier ++ if not under1000: ++ rounded_start = max(rounded_start, 1000) ++ else: ++ rounded_start = max(rounded_start, 1) ++ rounded_end = ((end + multiplier) // multiplier) * multiplier - 1 ++ ++ return rounded_start, rounded_end ++ ++ ++def get_rangename_base(id_ranges: List[IDRange]) -> str: ++ """Function to get a base name for new range proposals""" ++ base_name = "" ++ # we want to use default range name as a base for new ranges ++ for id_range in id_ranges: ++ if id_range.base_rid == 1000: ++ base_name = id_range.name ++ ++ # if we didn't find it, propose generic name ++ if base_name == "": ++ base_name = "Auto_added_range" ++ ++ return base_name ++ ++ ++def get_rangename(id_ranges: List[IDRange], basename: str) -> str: ++ """ ++ Function to get a new range name, we add the counter as 3-digit number ++ extension and make sure it's unique ++ """ ++ counter = 1 ++ full_name = f"{basename}_{counter:03}" ++ while any(id_range.name == full_name for id_range in id_ranges): ++ counter += 1 ++ full_name = f"{basename}_{counter:03}" ++ return full_name ++ ++ ++def propose_range( ++ group: List[IDentity], ++ id_ranges: List[IDRange], ++ delta: int, ++ basename: str, ++ norounding: bool, ++ allowunder1000: bool ++) -> IDRange: ++ """Function to propose a new range for group of IDs out of ranges""" ++ startid = group[0].number ++ endid = group[-1].number ++ ++ logger.debug( ++ "Proposing a range for existing IDs out of ranges with start id %s \ ++and end id %s...", ++ startid, ++ endid, ++ ) ++ ++ # creating new range ++ newrange = IDRange() ++ newrange.type = "ipa-local" ++ newrange.name = get_rangename(id_ranges, basename) ++ newrange.suffix = id_ranges[0].suffix ++ newrange.dn = f"cn={newrange.name},cn=ranges,cn=etc,{newrange.suffix}" ++ ++ if norounding: ++ newrange.first_id = startid ++ newrange.last_id = endid ++ newrange.size = newrange.last_id - newrange.first_id + 1 ++ else: ++ # first trying to round up ranges to look pretty ++ newrange.first_id, newrange.last_id = round_idrange( ++ startid, ++ endid, ++ allowunder1000 ++ ) ++ newrange.size = newrange.last_id - newrange.first_id + 1 ++ ++ # if this creates an overlap, try without rounding ++ if not newrange_overlap_check(id_ranges, newrange): ++ newrange.first_id = startid ++ newrange.last_id = endid ++ newrange.size = newrange.last_id - newrange.first_id + 1 ++ # if we still failed, abandon idea ++ if not newrange_overlap_check(id_ranges, newrange): ++ logger.error( ++ "ERROR! Failed to create idrange for existing IDs out of \ ++ranges with start id %s and end id %s, it overlaps with existing range!", ++ startid, ++ endid, ++ ) ++ return None ++ ++ # creating RID bases ++ ipa_local_ranges = get_ipa_local_ranges(id_ranges) ++ ++ result, proposed_base_rid = propose_rid_base( ++ newrange, ipa_local_ranges, delta, True ++ ) ++ if result: ++ newrange.base_rid = proposed_base_rid ++ newrange.last_base_rid = proposed_base_rid + newrange.size ++ else: ++ # if this fails we print the warning ++ logger.warning( ++ "Warning! Proposed base RIDs %s for new range start id %s and \ ++end id %s both failed, please adjust manually", ++ proposed_base_rid, ++ newrange.first_id, ++ newrange.last_id, ++ ) ++ ++ result, proposed_secondary_base_rid = propose_rid_base( ++ newrange, ipa_local_ranges, delta, False, proposed_base_rid ++ ) ++ if result: ++ newrange.secondary_base_rid = proposed_secondary_base_rid ++ newrange.last_secondary_rid = ( ++ proposed_secondary_base_rid + newrange.size ++ ) ++ else: ++ # if this fails we print the warning ++ logger.warning( ++ "Warning! Proposed secondary base RIDs %s for new range start id \ ++%s and end id %s both failed, please adjust manually", ++ proposed_secondary_base_rid, ++ newrange.first_id, ++ newrange.last_id, ++ ) ++ ++ logger.debug("Proposed range: %s", newrange) ++ return newrange ++# endregion +diff --git a/ipatests/test_integration/test_ipa_idrange_fix.py b/ipatests/test_integration/test_ipa_idrange_fix.py +new file mode 100644 +index 0000000000000000000000000000000000000000..de3da9bfd221ce74f1d1bbb0dbe12e4db08b8daa +--- /dev/null ++++ b/ipatests/test_integration/test_ipa_idrange_fix.py +@@ -0,0 +1,189 @@ ++# ++# Copyright (C) 2024 FreeIPA Contributors see COPYING for license ++# ++ ++""" ++Module provides tests for ipa-idrange-fix CLI. ++""" ++ ++import logging ++import re ++ ++from ipatests.pytest_ipa.integration import tasks ++from ipatests.test_integration.base import IntegrationTest ++ ++ ++logger = logging.getLogger(__name__) ++ ++ ++class TestIpaIdrangeFix(IntegrationTest): ++ @classmethod ++ def install(cls, mh): ++ super(TestIpaIdrangeFix, cls).install(mh) ++ tasks.kinit_admin(cls.master) ++ ++ def test_no_issues(self): ++ """Test ipa-idrange-fix command with no issues.""" ++ result = self.master.run_command(["ipa-idrange-fix", "--unattended"]) ++ ++ expected_under1000 = "No IDs under 1000 found" ++ expected_nochanges = "No changes proposed for existing ranges" ++ expected_newrange = "No new ranges proposed" ++ expected_noissues = "No changes proposed, nothing to do." ++ assert expected_under1000 in result.stderr_text ++ assert expected_nochanges in result.stderr_text ++ assert expected_newrange in result.stderr_text ++ assert expected_noissues in result.stderr_text ++ ++ def test_idrange_no_rid_bases(self): ++ """Test ipa-idrange-fix command with IDrange with no RID bases.""" ++ self.master.run_command([ ++ "ipa", ++ "idrange-add", ++ "idrange_no_rid_bases", ++ "--base-id", '10000', ++ "--range-size", '20000', ++ ]) ++ ++ result = self.master.run_command(["ipa-idrange-fix", "--unattended"]) ++ expected_text = "RID bases updated for range 'idrange_no_rid_bases'" ++ ++ # Remove IDrange with no rid bases ++ self.master.run_command(["ipa", "idrange-del", "idrange_no_rid_bases"]) ++ ++ assert expected_text in result.stderr_text ++ ++ def test_idrange_no_rid_bases_reversed(self): ++ """ ++ Test ipa-idrange-fix command with IDrange with no RID bases, but we ++ previously had a range with RID bases reversed - secondary lower than ++ primary. It is a valid configuration, so we should fix no-RID range. ++ """ ++ self.master.run_command([ ++ "ipa", ++ "idrange-add", ++ "idrange_no_rid_bases", ++ "--base-id", '10000', ++ "--range-size", '20000', ++ ]) ++ self.master.run_command([ ++ "ipa", ++ "idrange-add", ++ "idrange_reversed", ++ "--base-id", '50000', ++ "--range-size", '20000', ++ "--rid-base", '100300000' ++ "--secondary-rid-base", '301000' ++ ]) ++ ++ result = self.master.run_command(["ipa-idrange-fix", "--unattended"]) ++ expected_text = "RID bases updated for range 'idrange_no_rid_bases'" ++ ++ # Remove test IDranges ++ self.master.run_command(["ipa", "idrange-del", "idrange_no_rid_bases"]) ++ self.master.run_command(["ipa", "idrange-del", "idrange_reversed"]) ++ ++ assert expected_text in result.stderr_text ++ ++ def test_users_outofrange(self): ++ """Test ipa-idrange-fix command with users out of range.""" ++ for i in range(1, 20): ++ self.master.run_command([ ++ "ipa", ++ "user-add", ++ "testuser{}".format(i), ++ "--first", "Test", ++ "--last", "User {}".format(i), ++ "--uid", str(100000 + i * 10), ++ ]) ++ ++ result = self.master.run_command(["ipa-idrange-fix", "--unattended"]) ++ expected_text = r"Range '[\w\.]+_id_range_\d{3}' created successfully" ++ match = re.search(expected_text, result.stderr_text) ++ ++ # Remove users out of range and created IDrange ++ for i in range(1, 20): ++ self.master.run_command([ ++ "ipa", ++ "user-del", ++ "testuser{}".format(i) ++ ]) ++ if match is not None: ++ self.master.run_command([ ++ "ipa", ++ "idrange-del", ++ match.group(0).split(" ")[1].replace("'", "") ++ ]) ++ ++ assert match is not None ++ ++ def test_user_outlier(self): ++ """Test ipa-idrange-fix command with outlier user.""" ++ self.master.run_command([ ++ "ipa", ++ "user-add", ++ "testuser_outlier", ++ "--first", "Outlier", ++ "--last", "User", ++ "--uid", '500000', ++ ]) ++ ++ result = self.master.run_command(["ipa-idrange-fix", "--unattended"]) ++ expected_text = "Identities that don't fit the criteria to get a new \ ++range found!" ++ expected_user = "user 'Outlier User', uid=500000" ++ ++ # Remove outlier user ++ self.master.run_command(["ipa", "user-del", "testuser_outlier"]) ++ ++ assert expected_text in result.stderr_text ++ assert expected_user in result.stderr_text ++ ++ def test_user_under1000(self): ++ """Test ipa-idrange-fix command with user under 1000.""" ++ self.master.run_command([ ++ "ipa", ++ "user-add", ++ "testuser_under1000", ++ "--first", "Under", ++ "--last", "1000", ++ "--uid", '999', ++ ]) ++ ++ result = self.master.run_command(["ipa-idrange-fix", "--unattended"]) ++ expected_text = "IDs under 1000:" ++ expected_user = "user 'Under 1000', uid=999" ++ ++ # Remove user under 1000 ++ self.master.run_command(["ipa", "user-del", "testuser_under1000"]) ++ ++ assert expected_text in result.stderr_text ++ assert expected_user in result.stderr_text ++ ++ def test_user_preserved(self): ++ """Test ipa-idrange-fix command with preserved user.""" ++ self.master.run_command([ ++ "ipa", ++ "user-add", ++ "testuser_preserved", ++ "--first", "Preserved", ++ "--last", "User", ++ "--uid", '9999', ++ ]) ++ self.master.run_command([ ++ "ipa", ++ "user-del", ++ "testuser_preserved", ++ "--preserve" ++ ]) ++ ++ result = self.master.run_command(["ipa-idrange-fix", "--unattended"]) ++ expected_text = "Identities that don't fit the criteria to get a new \ ++range found!" ++ expected_user = "user 'Preserved User', uid=9999" ++ ++ # Remove preserved user ++ self.master.run_command(["ipa", "user-del", "testuser_preserved"]) ++ ++ assert expected_text in result.stderr_text ++ assert expected_user in result.stderr_text +-- +2.46.2 + diff --git a/0006-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch b/0006-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch new file mode 100644 index 0000000..a16cc1b --- /dev/null +++ b/0006-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch @@ -0,0 +1,36 @@ +From 4fef80aeaaf017b286bd12ebfc30529f6a65a80e Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Mon, 2 Sep 2024 18:28:27 +0200 +Subject: [PATCH] ipatests: Add missing comma in + test_idrange_no_rid_bases_reversed + +The test is calling ipa idrange-add but is missing a comma in +the arguments list. +The resulting call is using "--rid-base 100300000--secondary-rid-base". +Add the missing comma to build the command with +"--rid-base 100300000 --secondary-rid-base" + +Fixes: https://pagure.io/freeipa/issue/9656 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Alexander Bokovoy +--- + ipatests/test_integration/test_ipa_idrange_fix.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ipatests/test_integration/test_ipa_idrange_fix.py b/ipatests/test_integration/test_ipa_idrange_fix.py +index de3da9bfd221ce74f1d1bbb0dbe12e4db08b8daa..ff8fbdac9d028d26fc55f5e357f89af879a61723 100644 +--- a/ipatests/test_integration/test_ipa_idrange_fix.py ++++ b/ipatests/test_integration/test_ipa_idrange_fix.py +@@ -72,7 +72,7 @@ class TestIpaIdrangeFix(IntegrationTest): + "idrange_reversed", + "--base-id", '50000', + "--range-size", '20000', +- "--rid-base", '100300000' ++ "--rid-base", '100300000', + "--secondary-rid-base", '301000' + ]) + +-- +2.46.2 + diff --git a/0007-ipatests-Update-ipa-adtrust-install-test.patch b/0007-ipatests-Update-ipa-adtrust-install-test.patch new file mode 100644 index 0000000..1b809f2 --- /dev/null +++ b/0007-ipatests-Update-ipa-adtrust-install-test.patch @@ -0,0 +1,32 @@ +From a18eb8358675b3697ccf8f8d8dc230cc62df6a4d Mon Sep 17 00:00:00 2001 +From: Erik Belko +Date: Thu, 29 Aug 2024 16:47:21 +0200 +Subject: [PATCH] ipatests: Update ipa-adtrust-install test + +update test_user_connects_smb_share_if_locked_specific_group with wait +for SSSD to be online after ipa-adtrust-install command + +Related: https://pagure.io/freeipa/issue/9655 + +Signed-off-by: Erik Belko +Reviewed-By: Alexander Bokovoy +--- + ipatests/test_integration/test_adtrust_install.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/ipatests/test_integration/test_adtrust_install.py b/ipatests/test_integration/test_adtrust_install.py +index 72e8d874fb17adadc556ba55b825a88a3ac21a67..de252db1705ad940c3b5ee4df967d7c17a4203a7 100644 +--- a/ipatests/test_integration/test_adtrust_install.py ++++ b/ipatests/test_integration/test_adtrust_install.py +@@ -853,6 +853,8 @@ class TestIpaAdTrustInstall(IntegrationTest): + self.master.config.admin_password, + "-U"] + ) ++ # Wait for SSSD to become online before doing any other check ++ tasks.wait_for_sssd_domain_status_online(self.master) + self.master.run_command(["mkdir", "/freeipa4234"]) + self.master.run_command( + ["chcon", "-t", "samba_share_t", +-- +2.46.2 + diff --git a/0008-Installer-activate-ssh-service-in-sssd.conf.patch b/0008-Installer-activate-ssh-service-in-sssd.conf.patch new file mode 100644 index 0000000..d13e82c --- /dev/null +++ b/0008-Installer-activate-ssh-service-in-sssd.conf.patch @@ -0,0 +1,33 @@ +From 373d41f211c1a04dc432a068bc7d2ba825ff554c Mon Sep 17 00:00:00 2001 +From: Francisco Trivino +Date: Tue, 13 Aug 2024 12:44:21 +0200 +Subject: [PATCH] Installer: activate ssh service in sssd.conf + +This commit enables SSSD's ssh service in ipa-client-install to ensure +sss_ssh_knownhosts and sss_ssh_knownhostsproxy functions properly. + +Fixes: https://pagure.io/freeipa/issue/9649 +Related: https://pagure.io/freeipa/issue/9536 + +Signed-off-by: Francisco Trivino +Reviewed-By: Rob Crittenden +--- + ipaclient/install/client.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py +index 802db9614b24553b2b49259f3aebb366093560ac..47a371f629f6ddfb1cd5e9fff9faad737aa01f54 100644 +--- a/ipaclient/install/client.py ++++ b/ipaclient/install/client.py +@@ -974,6 +974,8 @@ def configure_sssd_conf( + + sssd_enable_service(sssdconfig, 'nss') + sssd_enable_service(sssdconfig, 'pam') ++ if options.conf_ssh: ++ sssd_enable_service(sssdconfig, 'ssh') + + domain.set_option('ipa_domain', cli_domain) + domain.set_option('ipa_hostname', client_hostname) +-- +2.46.2 + diff --git a/0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch b/0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch new file mode 100644 index 0000000..3ae8ee5 --- /dev/null +++ b/0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch @@ -0,0 +1,413 @@ +From 8d242ba741ec22b258d5e70a530cefd0940783c7 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Tue, 23 Jul 2024 17:07:06 -0400 +Subject: [PATCH] ipa-migrate - fix migration issues with entries using + ipaUniqueId in the RDN + +We need to handle these entries differently and specify what attribute +and search base to use to find the entry on the local server. Most +entries can use the "cn" attribute but for selinux usermaps we need to +search using the ipaOwner attribute which is a DN, and in turn requires +additional handling/converting in order to properly check if the usermap +exists or not. + +Also fixed an issue where an attribute should be removed from the local +entry if it does not exist on the remote entry. + +And fixed the handling od "sudoOrder" which is defined as multi-valued +in the schema, but we really need to treat it as single-valued + +Fixes: https://pagure.io/freeipa/issue/9640 + +Signed-off-by: Mark Reynolds +Reviewed-By: Rob Crittenden +Reviewed-By: Rob Crittenden +--- + ipaserver/install/ipa_migrate.py | 119 +++++++++++++++++++-- + ipaserver/install/ipa_migrate_constants.py | 84 +++++++++++++-- + 2 files changed, 188 insertions(+), 15 deletions(-) + +diff --git a/ipaserver/install/ipa_migrate.py b/ipaserver/install/ipa_migrate.py +index e21937401b3463335d8297b41a403405071d3795..78c530f24fe5d8c9f5de0f816df9904bf30c7b94 100644 +--- a/ipaserver/install/ipa_migrate.py ++++ b/ipaserver/install/ipa_migrate.py +@@ -32,7 +32,7 @@ from ipaserver.install.ipa_migrate_constants import ( + DS_CONFIG, DB_OBJECTS, DS_INDEXES, BIND_DN, LOG_FILE_NAME, + STRIP_OP_ATTRS, STRIP_ATTRS, STRIP_OC, PROD_ATTRS, + DNA_REGEN_VAL, DNA_REGEN_ATTRS, NIS_PLUGIN, IGNORE_ATTRS, +- DB_EXCLUDE_TREES ++ DB_EXCLUDE_TREES, POLICY_OP_ATTRS + ) + + """ +@@ -529,6 +529,14 @@ class IPAMigrate(): + # + # Helper functions + # ++ def attr_is_operational(self, attr): ++ schema = self.local_conn.schema ++ attr_obj = schema.get_obj(ldap.schema.AttributeType, attr) ++ if attr_obj is not None: ++ if attr_obj.usage == 1: ++ return True ++ return False ++ + def replace_suffix(self, entry_dn): + """ + Replace the base DN in an entry DN +@@ -1122,6 +1130,18 @@ class IPAMigrate(): + stats['reset_range'] += 1 + return entry + ++ def attr_is_required(self, attr, entry): ++ """ ++ Check if an attribute is required in this entry ++ """ ++ entry_oc = entry['objectClass'] ++ for oc in entry_oc: ++ required_attrs = self.local_conn.get_allowed_attributes( ++ [oc], raise_on_unknown=False, attributes="must") ++ if attr.lower() in required_attrs: ++ return True ++ return False ++ + def clean_entry(self, entry_dn, entry_type, entry_attrs): + """ + Clean up the entry from the remote server +@@ -1311,7 +1331,17 @@ class IPAMigrate(): + f"'{old_value}' " + "new value " + f"'{local_entry[attr][0]}'") +- ++ elif 'single' == sp_attr[1]: ++ # The attribute is defined as multivalued, but ++ # we really need to treat it as single valued ++ self.log_debug("Entry is different and will " ++ f"be updated: '{local_dn}' " ++ f"attribute '{attr}' replaced " ++ "with val " ++ f"'{remote_attrs[attr][0]}' " ++ "old value: " ++ f"{local_entry[attr][0]}") ++ local_entry[attr][0] = remote_attrs[attr][0] + goto_next_attr = True + break + +@@ -1358,6 +1388,31 @@ class IPAMigrate(): + local_entry[attr] = remote_attrs[attr] + entry_updated = True + ++ # Remove attributes in the local entry that do not exist in the ++ # remote entry ++ remove_attrs = [] ++ for attr in local_entry: ++ if (self.attr_is_operational(attr) ++ and attr.lower() not in POLICY_OP_ATTRS) or \ ++ attr.lower() in IGNORE_ATTRS or \ ++ attr.lower() in STRIP_ATTRS or \ ++ attr.lower() == "usercertificate": ++ # This is an attribute that we do not want to remove ++ continue ++ ++ if attr not in remote_attrs and \ ++ not self.attr_is_required(attr, local_entry): ++ # Mark this attribute for deletion ++ remove_attrs.append(attr) ++ entry_updated = True ++ ++ # Remove attributes ++ for remove_attr in remove_attrs: ++ self.log_debug("Entry is different and will be updated: " ++ f"'{local_dn}' attribute '{remove_attr}' " ++ "is being removed") ++ del local_entry[remove_attr] ++ + if range_reset: + stats['reset_range'] += 1 + +@@ -1371,6 +1426,9 @@ class IPAMigrate(): + """ + Process chunks of remote entries from a paged results search + ++ entry_dn = the remote entry DN ++ entry_attrs = the remote entry's attributes stored in a dict ++ + Identify entry type + Process entry (removing/change attr/val/schema) + Compare processed remote entry with local entry, merge/overwrite? +@@ -1426,6 +1484,47 @@ class IPAMigrate(): + # Based on the entry type do additional work + # + ++ # For entries with alternate identifying needs we need to rebuild the ++ # local dn. Typically this is for entries that use ipaUniqueId as the ++ # RDN attr ++ if entry_type != "custom" and 'alt_id' in DB_OBJECTS[entry_type]: ++ attr = DB_OBJECTS[entry_type]['alt_id']['attr'] ++ base = DB_OBJECTS[entry_type]['alt_id']['base'] ++ srch_filter = f'{attr}={entry_attrs[attr][0]}' ++ if DB_OBJECTS[entry_type]['alt_id']['isDN'] is True: ++ # Convert the filter to match the local suffix ++ srch_filter = self.replace_suffix(srch_filter) ++ srch_base = base + str(self.local_suffix) ++ ++ try: ++ entries = self.local_conn.get_entries(DN(srch_base), ++ filter=srch_filter) ++ if len(entries) == 1: ++ local_dn = entries[0].dn ++ elif len(entries) == 0: ++ # Not found, no problem just proceed and we will add it ++ pass ++ else: ++ # Found too many entries - should not happen ++ self.log_error('Found too many local matching entries ' ++ f'for "{local_dn}"') ++ if self.args.force: ++ stats['ignored_errors'] += 1 ++ return ++ else: ++ sys.exit(1) ++ except errors.EmptyResult: ++ # Not found, no problem just proceed and we will add it later ++ pass ++ except (errors.NetworkError, errors.DatabaseError) as e: ++ self.log_error('Failed to find a local matching entry for ' ++ f'"{local_dn}" error: {str(e)}') ++ if self.args.force: ++ stats['ignored_errors'] += 1 ++ return ++ else: ++ sys.exit(1) ++ + # See if the entry exists on the local server + try: + local_entry = self.local_conn.get_entry(DN(local_dn), +@@ -1441,14 +1540,20 @@ class IPAMigrate(): + + if self.dryrun: + self.write_update_to_ldif(local_entry) +- DB_OBJECTS[entry_type]['count'] += 1 ++ if entry_type == "custom": ++ stats['custom'] += 1 ++ else: ++ DB_OBJECTS[entry_type]['count'] += 1 + stats['total_db_migrated'] += 1 + return + + # Update the local entry + try: + self.local_conn.update_entry(local_entry) +- DB_OBJECTS[entry_type]['count'] += 1 ++ if entry_type == "custom": ++ stats['custom'] += 1 ++ else: ++ DB_OBJECTS[entry_type]['count'] += 1 + except errors.ExecutionError as e: + self.log_error(f'Failed to update "{local_dn}" error: ' + f'{str(e)}') +@@ -1567,7 +1672,7 @@ class IPAMigrate(): + """ + Used paged search for online method to avoid large memory footprint + """ +- self.log_info("Migrating database ... (this make take a while)") ++ self.log_info("Migrating database ... (this may take a while)") + if self.args.db_ldif is not None: + self.processDBOffline() + else: +@@ -1608,7 +1713,7 @@ class IPAMigrate(): + f"{len(objectclasses)} objectClasses") + + # Loop over attributes and objectclasses and count them +- schema = self.local_conn._get_schema() ++ schema = self.local_conn.schema + local_schema = schema.ldap_entry() + for schema_type in [(attributes, "attributeTypes"), + (objectclasses, "objectClasses")]: +@@ -1967,7 +2072,7 @@ class IPAMigrate(): + + # Run ipa-server-upgrade + self.log_info("Running ipa-server-upgrade ... " +- "(this make take a while)") ++ "(this may take a while)") + if self.dryrun: + self.log_info("Skipping ipa-server-upgrade in dryrun mode.") + else: +diff --git a/ipaserver/install/ipa_migrate_constants.py b/ipaserver/install/ipa_migrate_constants.py +index 0e26c75497b216f09ed450aa25a09c2102582326..250f1b5b01bf066d316a98489ab6153b89615173 100644 +--- a/ipaserver/install/ipa_migrate_constants.py ++++ b/ipaserver/install/ipa_migrate_constants.py +@@ -19,6 +19,28 @@ STRIP_OP_ATTRS = [ + 'nsuniqueid', + 'dsentrydn', + 'entryuuid', ++ 'entrydn', ++ 'entryid', ++ 'entryusn', ++ 'numsubordinates', ++ 'parentid', ++ 'tombstonenumsubordinates' ++] ++ ++# Operational attributes that we would want to remove from the local entry if ++# they don't exist in the remote entry ++POLICY_OP_ATTRS = [ ++ 'nsaccountlock', ++ 'passwordexpiratontime', ++ 'passwordgraceusertime', ++ 'pwdpolicysubentry', ++ 'passwordexpwarned', ++ 'passwordretrycount', ++ 'retrycountresettime', ++ 'accountunlocktime', ++ 'passwordhistory', ++ 'passwordallowchangetime', ++ 'pwdreset' + ] + + # Atributes to strip from users/groups +@@ -110,7 +132,7 @@ STRIP_OC = [ + # + # The DS_CONFIG mapping breaks each config entry (or type of entry) into its + # own catagory. Each catagory, or type, as DN list "dn", the attributes# we +-# are intrested in. These attributes are broken into singel valued "attrs", ++# are intrested in. These attributes are broken into single valued "attrs", + # or multi-valued attributes "multivalued". If the attributes is single + # valued then the value is replaced, if it's multivalued then it is "appended" + # +@@ -503,7 +525,7 @@ DS_CONFIG = { + } + + # +-# Slpai NIS is an optional plugin. It requires special handling ++# Slapi NIS is an optional plugin. It requires special handling + # + NIS_PLUGIN = { + 'dn': 'cn=NIS Server,cn=plugins,cn=config', +@@ -565,6 +587,12 @@ DS_INDEXES = { + # identify the entry. + # The "label" and "count" attributes are used for the Summary Report + # ++# Some entries use ipaUniqueId as the RDN attribute, this makes comparing ++# entries between the remote and local servers problematic. So we need special ++# identifying information to find the local entry. In this case we use the ++# "alt_id" key which is a dict of an attribute 'attr' and partial base DN ++# 'base' - which is expected to end in a comma. ++# + DB_OBJECTS = { + # Plugins + 'automember_def': { +@@ -640,8 +668,8 @@ DB_OBJECTS = { + 'oc': ['ipaconfigobject', 'ipaguiconfig'], + 'subtree': 'cn=ipaconfig,cn=etc,$SUFFIX', + 'special_attrs': [ +- # needs special handling, but +- # ipa-server-upgrade rewrites this attribute anyway! ++ # needs special handling, but ipa-server-upgrade rewrites this ++ # attribute anyway! + ('ipausersearchfields', 'list'), + ], + 'label': 'IPA Config', +@@ -772,11 +800,16 @@ DB_OBJECTS = { + 'mode': 'all', + 'count': 0, + }, +- 'subids': { # unknown what these entries look like TODO ++ 'subids': { + 'oc': [], + 'subtree': ',cn=subids,cn=accounts,$SUFFIX', + 'label': 'Sub IDs', +- 'mode': 'all', # TODO Maybe production only? ++ 'mode': 'production', ++ 'alt_id': { ++ 'attr': 'ipaOwner', ++ 'isDN': True, ++ 'base': 'cn=subids,cn=accounts,', ++ }, + 'count': 0, + }, + +@@ -884,6 +917,11 @@ DB_OBJECTS = { + 'oc': ['ipahbacrule'], + 'subtree': ',cn=hbac,$SUFFIX', + 'label': 'HBAC Rules', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=hbac,', ++ 'isDN': False, ++ }, + 'mode': 'all', + 'count': 0, + }, +@@ -892,6 +930,11 @@ DB_OBJECTS = { + 'selinux_usermap': { # Not sure if this is needed, entry is empty TODO + 'oc': [], + 'subtree': ',cn=usermap,cn=selinux,$SUFFIX', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=usermap,cn=selinux,', ++ 'isDN': False, ++ }, + 'label': 'Selinux Usermaps', + 'mode': 'all', + 'count': 0, +@@ -902,12 +945,27 @@ DB_OBJECTS = { + 'oc': ['ipasudorule'], + 'subtree': ',cn=sudorules,cn=sudo,$SUFFIX', + 'label': 'Sudo Rules', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=sudorules,cn=sudo,', ++ 'isDN': False, ++ }, ++ 'special_attrs': [ ++ # schema defines sudoOrder as mutlivalued, but we need to treat ++ # it as single valued ++ ('sudoorder', 'single'), ++ ], + 'mode': 'all', + 'count': 0, + }, + 'sudo_cmds': { + 'oc': ['ipasudocmd'], + 'subtree': ',cn=sudocmds,cn=sudo,$SUFFIX', ++ 'alt_id': { ++ 'attr': 'sudoCmd', ++ 'base': 'cn=sudocmds,cn=sudo,', ++ 'isDN': False, ++ }, + 'label': 'Sudo Commands', + 'mode': 'all', + 'count': 0, +@@ -991,6 +1049,11 @@ DB_OBJECTS = { + 'oc': ['ipanisnetgroup'], + 'not_oc': ['mepmanagedentry'], + 'subtree': ',cn=ng,cn=alt,$SUFFIX', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=ng,cn=alt,', ++ 'isDN': False, ++ }, + 'label': 'Network Groups', + 'mode': 'all', + 'count': 0, +@@ -1006,9 +1069,14 @@ DB_OBJECTS = { + 'count': 0, + }, + 'caacls': { +- 'oc': ['top'], ++ 'oc': ['ipacaacl'], + 'subtree': ',cn=caacls,cn=ca,$SUFFIX', +- 'label': 'CA Certificates', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=caacls,cn=ca,', ++ 'isDN': False, ++ }, ++ 'label': 'CA Certificate ACLs', + 'mode': 'all', + 'count': 0, + }, +-- +2.46.2 + diff --git a/0010-ipa-migrate-fix-alternate-entry-search-filter.patch b/0010-ipa-migrate-fix-alternate-entry-search-filter.patch new file mode 100644 index 0000000..c643d26 --- /dev/null +++ b/0010-ipa-migrate-fix-alternate-entry-search-filter.patch @@ -0,0 +1,68 @@ +From 3b5a980f5b65b03b9fd7ad0cfbb6c87874d3ff24 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Tue, 3 Sep 2024 13:42:05 -0400 +Subject: [PATCH] ipa-migrate - fix alternate entry search filter + +Processing a filter like a DN can cause normalization issues that result +in an invalid filter. Make sure the filter is encapsulated with +parenthesis and we call replace_suffix_value() instead of +replace_suffix() + +Fixes: https://pagure.io/freeipa/issue/9658 + +Signed-off-by: Mark Reynolds + +Fix typo in test + +Reviewed-By: Florence Blanc-Renaud +--- + ipaserver/install/ipa_migrate.py | 4 ++-- + ipatests/test_integration/test_ipa_ipa_migration.py | 6 +++--- + 2 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/ipaserver/install/ipa_migrate.py b/ipaserver/install/ipa_migrate.py +index 78c530f24fe5d8c9f5de0f816df9904bf30c7b94..38356aa23ea435e2a616f48356feaea7b50dd1e4 100644 +--- a/ipaserver/install/ipa_migrate.py ++++ b/ipaserver/install/ipa_migrate.py +@@ -1490,10 +1490,10 @@ class IPAMigrate(): + if entry_type != "custom" and 'alt_id' in DB_OBJECTS[entry_type]: + attr = DB_OBJECTS[entry_type]['alt_id']['attr'] + base = DB_OBJECTS[entry_type]['alt_id']['base'] +- srch_filter = f'{attr}={entry_attrs[attr][0]}' ++ srch_filter = f'({attr}={entry_attrs[attr][0]})' + if DB_OBJECTS[entry_type]['alt_id']['isDN'] is True: + # Convert the filter to match the local suffix +- srch_filter = self.replace_suffix(srch_filter) ++ srch_filter = self.replace_suffix_value(srch_filter) + srch_base = base + str(self.local_suffix) + + try: +diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py +index f697bbfbfc6169309274db689501c99fe148cc70..288165e8a83a96e6f6bd4e52866f98617f497c56 100644 +--- a/ipatests/test_integration/test_ipa_ipa_migration.py ++++ b/ipatests/test_integration/test_ipa_ipa_migration.py +@@ -610,7 +610,7 @@ class TestIPAMigrateScenario1(IntegrationTest): + MIGRATION_SCHEMA_LOG_MSG = "Migrating schema ...\n" + MIGRATION_CONFIG_LOG_MSG = "Migrating configuration ...\n" + IPA_UPGRADE_LOG_MSG = ( +- "Running ipa-server-upgrade ... (this make take a while)\n" ++ "Running ipa-server-upgrade ... (this may take a while)\n" + ) + SIDGEN_TASK_LOG_MSG = "Running SIDGEN task ...\n" + MIGRATION_COMPLETE_LOG_MSG = "Migration complete!\n" +@@ -641,10 +641,10 @@ class TestIPAMigrateScenario1(IntegrationTest): + tasks.kinit_admin(self.replicas[0]) + MIGRATION_SCHEMA_LOG_MSG = "Migrating schema ...\n" + MIGRATION_DATABASE_LOG_MSG = ( +- "Migrating database ... (this make take a while)\n" ++ "Migrating database ... (this may take a while)\n" + ) + IPA_UPGRADE_LOG_MSG = ( +- "Running ipa-server-upgrade ... (this make take a while)\n" ++ "Running ipa-server-upgrade ... (this may take a while)\n" + ) + SIDGEN_TASK_LOG_MSG = "Running SIDGEN task ...\n" + result = run_migrate( +-- +2.46.2 + diff --git a/0011-ipatests-provide-a-ccache-to-rpcclient-deletetrustdo.patch b/0011-ipatests-provide-a-ccache-to-rpcclient-deletetrustdo.patch new file mode 100644 index 0000000..127b1f7 --- /dev/null +++ b/0011-ipatests-provide-a-ccache-to-rpcclient-deletetrustdo.patch @@ -0,0 +1,67 @@ +From a343c149838a3058794f33c75c58b75bc1748f7f Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 17 Sep 2024 17:00:49 +0200 +Subject: [PATCH] ipatests: provide a ccache to rpcclient deletetrustdom + +With samba update to samba-4.20.4, rpcclient now needs a +ccache otherwise it prompts for a password. + +Fixes: https://pagure.io/freeipa/issue/9667 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Alexander Bokovoy +--- + ipatests/pytest_ipa/integration/tasks.py | 23 ++++++++++++++++++++--- + 1 file changed, 20 insertions(+), 3 deletions(-) + +diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py +index 9d6b5f67a311a28c335801d59e0ff0f0c7faccdd..677fb7534256a65940fb5280fa6412789dcba54f 100755 +--- a/ipatests/pytest_ipa/integration/tasks.py ++++ b/ipatests/pytest_ipa/integration/tasks.py +@@ -795,15 +795,22 @@ def remove_trust_info_from_ad(master, ad_domain, ad_hostname): + kinit_as_user(master, + 'Administrator@{}'.format(ad_domain.upper()), + master.config.ad_admin_password) ++ # Find cache for the user ++ cache_args = [] ++ cache = get_credential_cache(master) ++ if cache: ++ cache_args = ["--use-krb5-ccache", cache] ++ + # Detect whether rpcclient supports -k or --use-kerberos option + res = master.run_command(['rpcclient', '-h'], raiseonerr=False) + if "--use-kerberos" in res.stderr_text: + rpcclient_krb5_knob = "--use-kerberos=desired" + else: + rpcclient_krb5_knob = "-k" +- master.run_command(['rpcclient', rpcclient_krb5_knob, ad_hostname, +- '-c', 'deletetrustdom {}'.format(master.domain.name)], +- raiseonerr=False) ++ cmd_args = ['rpcclient', rpcclient_krb5_knob, ad_hostname] ++ cmd_args.extend(cache_args) ++ cmd_args.extend(['-c', 'deletetrustdom {}'.format(master.domain.name)]) ++ master.run_command(cmd_args, raiseonerr=False) + + + def configure_auth_to_local_rule(master, ad): +@@ -1086,6 +1093,16 @@ def kinit_admin(host, raiseonerr=True): + raiseonerr=raiseonerr) + + ++def get_credential_cache(host): ++ # Return the credential cache currently in use on host or None ++ result = host.run_command(["klist"]).stdout_text ++ pattern = re.compile(r'Ticket cache: (?P.*)\n') ++ res = pattern.search(result) ++ if res: ++ return res['cache'] ++ return None ++ ++ + def uninstall_master(host, ignore_topology_disconnect=True, + ignore_last_of_role=True, clean=True, verbose=False): + uninstall_cmd = ['ipa-server-install', '--uninstall', '-U'] +-- +2.46.2 + diff --git a/0012-test_adtrust_install-add-use-krb5-ccache-to-smbclien.patch b/0012-test_adtrust_install-add-use-krb5-ccache-to-smbclien.patch new file mode 100644 index 0000000..79600fd --- /dev/null +++ b/0012-test_adtrust_install-add-use-krb5-ccache-to-smbclien.patch @@ -0,0 +1,60 @@ +From 743c7b46e463bef666dc84e9f513eb7dee7f59f6 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 17 Sep 2024 14:48:58 +0200 +Subject: [PATCH] test_adtrust_install: add --use-krb5-ccache to smbclient + command + +With samba 4.20.4 the smbclient commands needs a ccache otherwise it +prompts for a password. + +Fixes: https://pagure.io/freeipa/issue/9666 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Alexander Bokovoy +--- + .../test_integration/test_adtrust_install.py | 28 ++++++++++++------- + 1 file changed, 18 insertions(+), 10 deletions(-) + +diff --git a/ipatests/test_integration/test_adtrust_install.py b/ipatests/test_integration/test_adtrust_install.py +index de252db1705ad940c3b5ee4df967d7c17a4203a7..79a91dfaa61276de74b10777c6d44b5942ed1be0 100644 +--- a/ipatests/test_integration/test_adtrust_install.py ++++ b/ipatests/test_integration/test_adtrust_install.py +@@ -873,17 +873,25 @@ class TestIpaAdTrustInstall(IntegrationTest): + "path", "/freeipa4234"]) + self.master.run_command(["touch", "before"]) + self.master.run_command(["touch", "after"]) +- self.master.run_command( +- ["smbclient", "--use-kerberos=desired", +- "-c=put before", "//{}/share".format( +- self.master.hostname)] +- ) ++ # Find cache for the admin user ++ cache_args = [] ++ cache = tasks.get_credential_cache(self.master) ++ if cache: ++ cache_args = ["--use-krb5-ccache", cache] ++ ++ cmd_args = ["smbclient", "--use-kerberos=desired"] ++ cmd_args.extend(cache_args) ++ cmd_args.extend([ ++ "-c=put before", "//{}/share".format(self.master.hostname) ++ ]) ++ self.master.run_command(cmd_args) + self.master.run_command( + ["net", "conf", "setparm", "share", + "valid users", "@admins"]) +- result = self.master.run_command( +- ["smbclient", "--use-kerberos=desired", +- "-c=put after", "//{}/share".format( +- self.master.hostname)] +- ) ++ cmd_args = ["smbclient", "--use-kerberos=desired"] ++ cmd_args.extend(cache_args) ++ cmd_args.extend([ ++ "-c=put after", "//{}/share".format(self.master.hostname) ++ ]) ++ result = self.master.run_command(cmd_args) + assert msg not in result.stdout_text +-- +2.46.2 + diff --git a/0013-Don-t-rely-on-removing-the-CA-to-uninstall-the-ACME-.patch b/0013-Don-t-rely-on-removing-the-CA-to-uninstall-the-ACME-.patch new file mode 100644 index 0000000..19498d0 --- /dev/null +++ b/0013-Don-t-rely-on-removing-the-CA-to-uninstall-the-ACME-.patch @@ -0,0 +1,209 @@ +From a785d0c561b8e22bd9d56739481095e07e0a7eb7 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 30 Sep 2024 13:30:46 -0400 +Subject: [PATCH] Don't rely on removing the CA to uninstall the ACME depoyment + +There has always been a pki-server commnd acme-remove. We were +not aware that it should be called prior to removing a CA. In +11.5.0 this is strongly encouraged by the PKI team. In 11.6.0 +ACME is treated as a full subsystem so will be removed in the +future using pkidestroy -s ACME + +The new class acmeinstance.ACMEInstance is introduced so its +uninstallation can be handled in a similar way as the other +PKI services via DogtagInstance. It is, right now, a pretty +thin wrapper. + +We can discuss moving the ACME installation routines here at +some point. It would be ok as long as we don't have to introduce +another PKI restart as part of it. + +In PKI 11.6.0 pkidestroy has new options to ensure a clean +uninstall: --remove-conf --remove-logs. Pass those options +into pkidestroy calls for 11.6.0+. + +Clean up an additional IPA-generated file that needs to be +cleaned up during uninstall: /root/kracert.p12. 11.6.0 is +more sensitive to leftover files than previous versions. + +Fixes: https://pagure.io/freeipa/issue/9673 +Fixes: https://pagure.io/freeipa/issue/9674 + +Signed-off-by: Rob Crittenden +Reviewed-By: Alexander Bokovoy +--- + ipaserver/install/acmeinstance.py | 31 +++++++++++++ + ipaserver/install/ca.py | 5 ++- + ipaserver/install/cainstance.py | 1 + + ipaserver/install/dogtaginstance.py | 44 +++++++++++++------ + .../test_integration/test_uninstallation.py | 21 +++++++++ + 5 files changed, 87 insertions(+), 15 deletions(-) + create mode 100644 ipaserver/install/acmeinstance.py + +diff --git a/ipaserver/install/acmeinstance.py b/ipaserver/install/acmeinstance.py +new file mode 100644 +index 0000000000000000000000000000000000000000..0027c314545f384d9b6ee24b279479e5360d8bef +--- /dev/null ++++ b/ipaserver/install/acmeinstance.py +@@ -0,0 +1,31 @@ ++# ++# Copyright (C) 2024 FreeIPA Contributors see COPYING for license ++# ++ ++import logging ++ ++from ipaserver.install.dogtaginstance import DogtagInstance ++ ++logger = logging.getLogger(__name__) ++ ++ ++class ACMEInstance(DogtagInstance): ++ """ ++ ACME is deployed automatically with a CA subsystem but it is the ++ responsibility of IPA to uninstall the service. ++ ++ This is mostly a placeholder for the uninstaller. We can ++ eventually move the ACME installation routines into this class ++ if we want but it might result in an extra PKI restart which ++ would be slow. ++ """ ++ def __init__(self, realm=None, host_name=None): ++ super(ACMEInstance, self).__init__( ++ realm=realm, ++ subsystem="ACME", ++ service_desc="ACME server", ++ host_name=host_name ++ ) ++ ++ def uninstall(self): ++ DogtagInstance.uninstall(self) +diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py +index ffcb5268399ce71128fc8de5f54d433d35e99dd2..520e3fc5de1084e7c22c0cf7eaa86e1d3c421373 100644 +--- a/ipaserver/install/ca.py ++++ b/ipaserver/install/ca.py +@@ -22,7 +22,7 @@ from ipaplatform.constants import constants + from ipaserver.install import sysupgrade + from ipapython.install import typing + from ipapython.install.core import group, knob, extend_knob +-from ipaserver.install import cainstance, bindinstance, dsinstance ++from ipaserver.install import acmeinstance, cainstance, bindinstance, dsinstance + from ipapython import ipautil, certdb + from ipapython import ipaldap + from ipapython.admintool import ScriptError +@@ -715,6 +715,9 @@ def install_step_1(standalone, replica_config, options, custodia): + + + def uninstall(): ++ acme = acmeinstance.ACMEInstance(api.env.realm) ++ acme.uninstall() ++ + ca_instance = cainstance.CAInstance(api.env.realm) + ca_instance.stop_tracking_certificates() + ipautil.remove_file(paths.RA_AGENT_PEM) +diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py +index 5dac2c0441752e7bb569cde1fc93bc17c3128cdf..5c2c9f8b981cf5d587865f7680e2b231eae655e2 100644 +--- a/ipaserver/install/cainstance.py ++++ b/ipaserver/install/cainstance.py +@@ -1118,6 +1118,7 @@ class CAInstance(DogtagInstance): + + ipautil.remove_file(paths.DOGTAG_ADMIN_P12) + ipautil.remove_file(paths.CACERT_P12) ++ ipautil.remove_file(paths.ADMIN_CERT_PATH) + + def unconfigure_certmonger_renewal_guard(self): + if not self.is_configured(): +diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py +index e89492312deb8ca20668a62fd7a2a20e2866a3fb..4b0f4d274b0c33140ed6f939f1a3fd8b75930ff9 100644 +--- a/ipaserver/install/dogtaginstance.py ++++ b/ipaserver/install/dogtaginstance.py +@@ -304,21 +304,37 @@ class DogtagInstance(service.Service): + if self.is_installed(): + self.print_msg("Unconfiguring %s" % self.subsystem) + +- args = [paths.PKIDESTROY, +- "-i", "pki-tomcat", "--force", +- "-s", self.subsystem] +- +- # specify --log-file on PKI 11.0.0 or later +- ++ args = [] + pki_version = pki.util.Version(pki.specification_version()) +- if pki_version >= pki.util.Version("11.0.0"): +- timestamp = time.strftime( +- "%Y%m%d%H%M%S", +- time.localtime(time.time())) +- log_file = os.path.join( +- paths.VAR_LOG_PKI_DIR, +- "pki-%s-destroy.%s.log" % (self.subsystem.lower(), timestamp)) +- args.extend(["--log-file", log_file]) ++ if self.subsystem == "ACME": ++ if pki_version < pki.util.Version("11.0.0"): ++ return ++ elif ( ++ pki.util.Version("11.0.0") <= pki_version ++ <= pki.util.Version("11.5.0") ++ ): ++ args = ['pki-server', 'acme-remove'] ++ else: ++ # fall through for PKI >= 11.6.0 ++ pass ++ if not args: ++ args = [paths.PKIDESTROY, ++ "-i", "pki-tomcat", "--force", ++ "-s", self.subsystem] ++ ++ # specify --log-file on PKI 11.0.0 or later ++ ++ if pki_version >= pki.util.Version("11.0.0"): ++ timestamp = time.strftime( ++ "%Y%m%d%H%M%S", ++ time.localtime(time.time())) ++ log_file = os.path.join( ++ paths.VAR_LOG_PKI_DIR, ++ "pki-%s-destroy.%s.log" % ++ (self.subsystem.lower(), timestamp)) ++ args.extend(["--log-file", log_file]) ++ if pki_version >= pki.util.Version("11.6.0"): ++ args.extend(["--remove-conf", "--remove-logs"]) + + try: + ipautil.run(args) +diff --git a/ipatests/test_integration/test_uninstallation.py b/ipatests/test_integration/test_uninstallation.py +index 4f8f17ce3ad8d5376ecba11442f379e5691de7f7..049c50db536ae1070f5f958e76b12a1518da0aba 100644 +--- a/ipatests/test_integration/test_uninstallation.py ++++ b/ipatests/test_integration/test_uninstallation.py +@@ -197,6 +197,7 @@ class TestUninstallCleanup(IntegrationTest): + '/var/lib/sss/pubconf/krb5.include.d/localauth_plugin', + '/var/named/dynamic/managed-keys.bind', + '/var/named/dynamic/managed-keys.bind.jnl', ++ '/var/lib/systemd/coredump/', + ] + + leftovers = [] +@@ -217,3 +218,23 @@ class TestUninstallCleanup(IntegrationTest): + leftovers.append(line) + + assert len(leftovers) == 0 ++ ++ ++class TestUninstallReinstall(IntegrationTest): ++ """Test install, uninstall, re-install. ++ ++ Reinstall with PKI 11.6.0 was failing ++ https://pagure.io/freeipa/issue/9673 ++ """ ++ ++ num_replicas = 0 ++ ++ @classmethod ++ def install(cls, mh): ++ tasks.install_master(cls.master, setup_dns=False) ++ ++ def test_uninstall_server(self): ++ tasks.uninstall_master(self.master) ++ ++ def test_reinstall_server(self): ++ tasks.install_master(self.master, setup_dns=False) +-- +2.46.2 + diff --git a/0014-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch b/0014-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch new file mode 100644 index 0000000..910e631 --- /dev/null +++ b/0014-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch @@ -0,0 +1,35 @@ +From ae4c2ad6cd966d48c063814f494dcc16cf0ccd4c Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Tue, 24 Sep 2024 13:46:48 +0530 +Subject: [PATCH] ipatests: Fixes for ipa-idrange-fix testsuite + +This patch adds the line tasks.install_master(cls.master). +The kinit admin command fails with the below error as the +IPA is not configured on the test system + +'ipa: ERROR: stderr: kinit: Configuration file does not specify default +realm when parsing name admin' + +Signed-off-by: Sudhir Menon +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_ipa_idrange_fix.py | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/ipatests/test_integration/test_ipa_idrange_fix.py b/ipatests/test_integration/test_ipa_idrange_fix.py +index ff8fbdac9d028d26fc55f5e357f89af879a61723..0c915bd0931ed11a3aa86c533ee8748aa8a7ec07 100644 +--- a/ipatests/test_integration/test_ipa_idrange_fix.py ++++ b/ipatests/test_integration/test_ipa_idrange_fix.py +@@ -17,6 +17,9 @@ logger = logging.getLogger(__name__) + + + class TestIpaIdrangeFix(IntegrationTest): ++ ++ topology = 'line' ++ + @classmethod + def install(cls, mh): + super(TestIpaIdrangeFix, cls).install(mh) +-- +2.46.2 + diff --git a/0015-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch b/0015-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch new file mode 100644 index 0000000..ca6e6da --- /dev/null +++ b/0015-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch @@ -0,0 +1,265 @@ +From 18303b94bea4e08a0c889fc357df6ba2f308fa0d Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Wed, 2 Oct 2024 21:26:34 -0400 +Subject: [PATCH] Do not let user with an expired OTP token to log in if only + OTP is allowed + +If only OTP authentication is allowed, and a user tries to login with an +expired token, do not let them log in with their password. Forcing the +admin to intervene. If the user does not have an OTP token then allow +them to log in with a password until an OTP token is configured + +Fixes: https://pagure.io/freeipa/issue/9387 + +Signed-off-by: Mark Reynolds +Reviewed-By: Rob Crittenden +Reviewed-By: Alexander Bokovoy +Reviewed-By: Julien Rische +--- + daemons/ipa-kdb/ipa_kdb_principals.c | 63 +++++++++++-- + .../ipa-slapi-plugins/ipa-pwd-extop/prepost.c | 3 +- + ipatests/test_integration/test_otp.py | 94 ++++++++++++++++++- + 3 files changed, 151 insertions(+), 9 deletions(-) + +diff --git a/daemons/ipa-kdb/ipa_kdb_principals.c b/daemons/ipa-kdb/ipa_kdb_principals.c +index 14603e528b43acb29234c425e97ad297ac6724a7..114957b884786dd3ca3b01c47f6bb82e8a040beb 100644 +--- a/daemons/ipa-kdb/ipa_kdb_principals.c ++++ b/daemons/ipa-kdb/ipa_kdb_principals.c +@@ -107,7 +107,6 @@ static char *std_principal_obj_classes[] = { + "krbprincipal", + "krbprincipalaux", + "krbTicketPolicyAux", +- + NULL + }; + +@@ -338,14 +337,16 @@ static void ipadb_validate_otp(struct ipadb_context *ipactx, + if (dn == NULL) + return; + count = asprintf(&filter, ftmpl, dn, datetime, datetime); +- ldap_memfree(dn); +- if (count < 0) ++ if (count < 0) { ++ ldap_memfree(dn); + return; ++ } + + /* Fetch the active token list. */ + kerr = ipadb_simple_search(ipactx, ipactx->base, LDAP_SCOPE_SUBTREE, + filter, (char**) attrs, &res); + free(filter); ++ filter = NULL; + if (kerr != 0 || res == NULL) + return; + +@@ -353,10 +354,60 @@ static void ipadb_validate_otp(struct ipadb_context *ipactx, + count = ldap_count_entries(ipactx->lcontext, res); + ldap_msgfree(res); + +- /* If the user is configured for OTP, but has no active tokens, remove +- * OTP from the list since the user obviously can't log in this way. */ +- if (count == 0) ++ /* ++ * If there are no valid tokens then we need to remove the OTP flag, ++ * unless OTP is the only auth type allowed... ++ */ ++ if (count == 0) { ++ /* Remove the OTP flag for now */ + *ua &= ~IPADB_USER_AUTH_OTP; ++ ++ if (*ua == 0) { ++ /* ++ * Ok, we "only" allow OTP, so if there is an expired/disabled ++ * token then add back the OTP flag as the server will double ++ * check the validity and reject the entire bind. Otherwise, this ++ * is the first time the user is authenticating and the user ++ * should be allowed to bind using its password ++ */ ++ static const char *expired_ftmpl = "(&" ++ "(objectClass=ipaToken)(ipatokenOwner=%s)" ++ "(|(ipatokenNotAfter<=%s)(!(ipatokenNotAfter=*))" ++ "(ipatokenDisabled=True))" ++ ")"; ++ if (asprintf(&filter, expired_ftmpl, dn, datetime) < 0) { ++ ldap_memfree(dn); ++ return; ++ } ++ ++ krb5_klog_syslog(LOG_INFO, ++ "Entry (%s) does not have a valid token and only OTP " ++ "authentication is supported, checking for expired tokens...", ++ dn); ++ ++ kerr = ipadb_simple_search(ipactx, ipactx->base, LDAP_SCOPE_SUBTREE, ++ filter, (char**) attrs, &res); ++ free(filter); ++ if (kerr != 0 || res == NULL) { ++ ldap_memfree(dn); ++ return; ++ } ++ ++ if (ldap_count_entries(ipactx->lcontext, res) > 0) { ++ /* ++ * Ok we only allow OTP, and there are expired/disabled tokens ++ * so add the OTP flag back, and the server will reject the ++ * bind ++ */ ++ krb5_klog_syslog(LOG_INFO, ++ "Entry (%s) does have an expired/disabled token so this " ++ "user can not fall through to password auth", dn); ++ *ua |= IPADB_USER_AUTH_OTP; ++ } ++ ldap_msgfree(res); ++ } ++ } ++ ldap_memfree(dn); + } + + static void ipadb_validate_radius(struct ipadb_context *ipactx, +diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c +index c967e2cfffbd920280639f3188783ec150523b47..1c1340e31ac30cb01412a7065ea339cb5461e839 100644 +--- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c ++++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c +@@ -1528,7 +1528,8 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb) + if (!syncreq && (otpreq == OTP_IS_NOT_REQUIRED)) { + ret = ipapwd_gen_checks(pb, &errMesg, &krbcfg, IPAPWD_CHECK_ONLY_CONFIG); + if (ret != 0) { +- LOG_FATAL("ipapwd_gen_checks failed!?\n"); ++ LOG_FATAL("ipapwd_gen_checks failed for '%s': %s\n", ++ slapi_sdn_get_dn(sdn), errMesg); + slapi_entry_free(entry); + slapi_sdn_free(&sdn); + return 0; +diff --git a/ipatests/test_integration/test_otp.py b/ipatests/test_integration/test_otp.py +index 350371bfe1e4c1cc6dcc89f6584f813fcb0d32a0..878b4fb560ba8d7768ead54b065656462545babd 100644 +--- a/ipatests/test_integration/test_otp.py ++++ b/ipatests/test_integration/test_otp.py +@@ -10,6 +10,7 @@ import re + import time + import textwrap + from urllib.parse import urlparse, parse_qs ++from paramiko import AuthenticationException + + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes +@@ -83,7 +84,7 @@ def kinit_otp(host, user, *, password, otp, success=True): + ) + + +-def ssh_2f(hostname, username, answers_dict, port=22): ++def ssh_2f(hostname, username, answers_dict, port=22, unwanted_prompt=""): + """ + :param hostname: hostname + :param username: username +@@ -103,6 +104,10 @@ def ssh_2f(hostname, username, answers_dict, port=22): + logger.info("Prompt is: '%s'", prmpt_str) + logger.info( + "Answer to ssh prompt is: '%s'", answers_dict[prmpt_str]) ++ if unwanted_prompt and prmpt_str == unwanted_prompt: ++ # We should not see this prompt ++ raise ValueError("We got an unwanted prompt: " ++ + answers_dict[prmpt_str]) + return resp + + import paramiko +@@ -193,7 +198,8 @@ class TestOTPToken(IntegrationTest): + + # skipping too many OTP fails + otp1 = hotp.generate(10).decode("ascii") +- kinit_otp(self.master, USER, password=PASSWORD, otp=otp1, success=False) ++ kinit_otp(self.master, USER, password=PASSWORD, otp=otp1, ++ success=False) + # Now the token is desynchronized + yield (otpuid, hotp) + +@@ -536,3 +542,87 @@ class TestOTPToken(IntegrationTest): + finally: + master.run_command(['ipa', 'pwpolicy-mod', '--minlife', '1']) + master.run_command(['ipa', 'user-del', USER1]) ++ ++ def test_totp_expired_ldap(self): ++ master = self.master ++ basedn = master.domain.basedn ++ USER1 = 'user-expired-otp' ++ TMP_PASSWORD = 'Secret1234509' ++ binddn = DN(f"uid={USER1},cn=users,cn=accounts,{basedn}") ++ controls = [ ++ BooleanControl( ++ controlType="2.16.840.1.113730.3.8.10.7", ++ booleanValue=True) ++ ] ++ ++ tasks.kinit_admin(master) ++ master.run_command(['ipa', 'pwpolicy-mod', '--minlife', '0']) ++ tasks.user_add(master, USER1, password=TMP_PASSWORD) ++ # Enforce use of OTP token for this user ++ master.run_command(['ipa', 'user-mod', USER1, ++ '--user-auth-type=otp']) ++ try: ++ # Change initial password through the IPA endpoint ++ url = f'https://{master.hostname}/ipa/session/change_password' ++ master.run_command(['curl', '-d', f'user={USER1}', ++ '-d', f'old_password={TMP_PASSWORD}', ++ '-d', f'new_password={PASSWORD}', ++ '--referer', f'https://{master.hostname}/ipa', ++ url]) ++ conn = master.ldap_connect() ++ # First, attempt authenticating with a password but without LDAP ++ # control to enforce OTP presence and without server-side ++ # enforcement of the OTP presence check. ++ conn.simple_bind(binddn, f"{PASSWORD}") ++ ++ # Add an OTP token and then modify it to be expired ++ otpuid, totp = add_otptoken(master, USER1, otptype="totp") ++ ++ # Make sure OTP auth is working ++ otpvalue = totp.generate(int(time.time())).decode("ascii") ++ conn = master.ldap_connect() ++ conn.simple_bind(binddn, f"{PASSWORD}{otpvalue}", ++ client_controls=controls) ++ conn.unbind() ++ ++ # Modfy token so that is now expired ++ args = [ ++ "ipa", ++ "otptoken-mod", ++ otpuid, ++ "--not-after", ++ "20241001010000Z", ++ ] ++ master.run_command(args) ++ ++ # Next, authenticate with Password+OTP again and with the LDAP ++ # control this operation should now fail ++ time.sleep(45) ++ otpvalue = totp.generate(int(time.time())).decode("ascii") ++ ++ conn = master.ldap_connect() ++ with pytest.raises(errors.ACIError): ++ conn.simple_bind(binddn, f"{PASSWORD}{otpvalue}", ++ client_controls=controls) ++ ++ # Sleep to make sure we are going to use a different token value ++ time.sleep(45) ++ ++ # Use OTP token again but authenticate over ssh and make sure it ++ # doesn't fallthrough to asking for a password ++ otpvalue = totp.generate(int(time.time())).decode("ascii") ++ answers = { ++ 'Enter first factor:': PASSWORD, ++ 'Enter second factor:': otpvalue ++ } ++ with pytest.raises(AuthenticationException): ++ # ssh should fail and NOT ask for a password ++ ssh_2f(master.hostname, USER1, answers, ++ unwanted_prompt="Password:") ++ ++ # Remove token ++ del_otptoken(self.master, otpuid) ++ ++ finally: ++ master.run_command(['ipa', 'pwpolicy-mod', '--minlife', '1']) ++ master.run_command(['ipa', 'user-del', USER1]) +-- +2.46.2 + diff --git a/0016-ipatests-Activate-ssh-in-sssd.conf.patch b/0016-ipatests-Activate-ssh-in-sssd.conf.patch new file mode 100644 index 0000000..9e24025 --- /dev/null +++ b/0016-ipatests-Activate-ssh-in-sssd.conf.patch @@ -0,0 +1,41 @@ +From 761647f842567713032709753b6d63467d9871a6 Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Mon, 23 Sep 2024 14:05:43 +0530 +Subject: [PATCH] ipatests: Activate ssh in sssd.conf + +This testcase checks that services: ssh +is included in the sssd.conf file when +ipa-client-install is successful. + +Ref: https://pagure.io/freeipa/issue/9649 + +Signed-off-by: Sudhir Menon +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_installation_client.py | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/ipatests/test_integration/test_installation_client.py b/ipatests/test_integration/test_installation_client.py +index f8567b39eead4dffd522aad504fa72a086969257..884bff2f2f3e49f66da391424e128d8e31b82a8a 100644 +--- a/ipatests/test_integration/test_installation_client.py ++++ b/ipatests/test_integration/test_installation_client.py +@@ -94,6 +94,16 @@ class TestInstallClient(IntegrationTest): + ).encode() not in krb5_cfg + tasks.uninstall_client(self.clients[0]) + ++ def test_check_ssh_service_is_activated(self): ++ """ ++ This test checks all default services are activated ++ in sssd.conf including ssh ++ """ ++ tasks.install_client(self.master, self.clients[0]) ++ sssd_cfg = self.clients[0].get_file_contents(paths.SSSD_CONF) ++ assert 'services = nss, pam, ssh, sudo' in sssd_cfg.decode() ++ tasks.uninstall_client(self.clients[0]) ++ + def test_install_with_automount(self): + """Test that installation with automount is successful""" + tasks.install_client(self.master, self.clients[0], +-- +2.46.2 + diff --git a/0017-ipa-migrate-man-page-fix-typos-and-errors.patch b/0017-ipa-migrate-man-page-fix-typos-and-errors.patch new file mode 100644 index 0000000..a48b5d4 --- /dev/null +++ b/0017-ipa-migrate-man-page-fix-typos-and-errors.patch @@ -0,0 +1,131 @@ +From f978fa05e3ed9d4ad9d20493c05c77fb9b4976a7 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 15 Oct 2024 17:04:55 +0200 +Subject: [PATCH] ipa-migrate man page: fix typos and errors + +ipa-migrate man page mentions non-existing option --hostname. +Fix the SYNOPSIS and various typos. + +Fixes: https://pagure.io/freeipa/issue/9681 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Mark Reynolds +--- + install/tools/man/ipa-migrate.1 | 38 +++++++++++++++------------------ + 1 file changed, 17 insertions(+), 21 deletions(-) + +diff --git a/install/tools/man/ipa-migrate.1 b/install/tools/man/ipa-migrate.1 +index 47ae47ea4afa3a5a6fe25dd9bbd14c27ab5f1fdb..5106f4f0f5b5928909ccd5abcef3bb6d1586f5df 100644 +--- a/install/tools/man/ipa-migrate.1 ++++ b/install/tools/man/ipa-migrate.1 +@@ -5,11 +5,11 @@ + .SH "NAME" + ipa\-migrate \- Migrate an IPA server from one machine to another + .SH "SYNOPSIS" +-ipa\-migrate ++\fBipa\-migrate\fR [OPTIONS] \fBprod\-mode\fR|\fBstage\-mode\fR \fIhostname\fR + .SH "DESCRIPTION" + + Use the \fIipa-migrate\fR command to migrate one +-IPA server to an existing local IPA server installation. ++IPA server \fIhostname\fR to an existing local IPA server installation. + + Migrate IPA schema, configuration, and database to a local IPA server. This + migration can be done online, where the tool will query the remote server. Or, +@@ -19,7 +19,6 @@ and then use an exported LDIF file for the database migration portion (this + might be more useful for very large databases as you don't need to worry about + network interruptions) + +-.SH POSITIONAL ARGUMENTS + .TP + \fBprod\-mode\fR + In this mode everything will be migrated including the current user SIDs and +@@ -28,13 +27,10 @@ DNA ranges + \fBstage\-mode\fR + In this mode, SIDs & DNA ranges are not migrated, and DNA attributes are reset + +-.SH "COMMANDS" ++.SH "OPTIONS" + .TP + \fB\-v\fR, \fB\-\-verbose\fR +-Use verbose output while running the migration tool. +-.TP +-\fB\-e\fR, \fB\-\-hostname=HOSTNAME\fR +-The host name of the remote IPA server that is being migrated from. ++Use verbose output while running the migration tool + .TP + \fB\-D\fR, \fB\-\-bind\-dn=BIND_DN\fR + The Bind DN (Distinguished Name) or an LDAP entry to bind to the remote IPA server with. +@@ -43,10 +39,10 @@ access to read the userPassword attribute. If ommitted the default is "cn=direc + .TP + \fB\-w\fR, \fB\-\-bind\-pw=PASSWORD\fR + The password for the Bind DN that is authenticating against the remote IPA server. If +-a password is not provided then the tool with prompt for the password if needed. ++a password is not provided then the tool with prompt for the password if needed + .TP +-\fB\-Just\fR, \fB\-\-bind\-pw\-file=FILE_PATH\fR +-Path to a file containing the password for the Bind DN. ++\fB\-j\fR, \fB\-\-bind\-pw\-file=FILE_PATH\fR ++Path to a file containing the password for the Bind DN + .TP + \fB\-Z\fR, \fB\-\-cacertfile=FILE_PATH\fR + Path to a file containing a CA Certificate that the remote server trusts +@@ -55,23 +51,23 @@ Path to a file containing a CA Certificate that the remote server trusts + Path to a file containing the migration log. By default the tool will use \fI/var/log/ipa-migrate.log\fR + .TP + \fB\-x\fR, \fB\-\-dryrun\fR +-Go through the migration process but do not write and data to the new IPA server. ++Go through the migration process but do not write any data to the new IPA server + .TP + \fB\-o\fR, \fB\-\-dryrun\-record=FILE_PATH\fR + Go through the migration process but do not write any data to the new IPA server. However, write the +-migration operations to an LDIF file which can be applied later or reused for multiple migrations. ++migration operations to an LDIF file which can be applied later or reused for multiple migrations + .TP + \fB\-r\fR, \fB\-\-reset\-range\fR + Reset the ID range for migrated users/groups. In "stage-mode" this is done automatically + .TP + \fB\-F\fR, \fB\-\-force\fR +-Ignore any errors and continue to proceed with migration effort. ++Ignore any errors and continue to proceed with migration effort + .TP + \fB\-q\fR, \fB\-\-quiet\fR +-Only log errors during the migration process. ++Only log errors during the migration process + .TP + \fB\-B\fR, \fB\-\-migrate\-dns\fR +-Migrate thr DNS records ++Migrate the DNS records + .TP + \fB\-S\fR, \fB\-\-skip\-schema\fR + Do not migrate the database schema +@@ -80,21 +76,21 @@ Do not migrate the database schema + Do not migrate the database configuration (dse.ldif/cn=config) + .TP + \fB\-O\fR, \fB\-\-schema\-overwrite\fR +-Overwrite existing schema definitions. By default duplicate schema is skipped. ++Overwrite existing schema definitions. By default duplicate schema is skipped + .TP + \fB\-s\fR, \fB\-\-subtree=DN\fR + Specifies a custom database subtree that should be included in the migration. + This is only needed if non-default subtrees/branches were added to the database +-outside of IPA. ++outside of IPA + .TP + \fB\-f\fR, \fB\-\-db\-ldif=FILE_PATH\fR +-LDIF file containing the entire backend. If omitted the tool will query the remote IPA server. ++LDIF file containing the entire backend. If omitted the tool will query the remote IPA server + .TP + \fB\-m\fR, \fB\-\-schema\-ldif=FILE_PATH\fR +-LDIF file containing the schema. If omitted the tool will query the remote IPA server. ++LDIF file containing the schema. If omitted the tool will query the remote IPA server + .TP + \fB\-g\fR, \fB\-\-config\-ldif=FILE_PATH\fR +-LDIF file containing the entire "cn=config" DIT. If omitted the tool will query the remote IPA server. ++LDIF file containing the entire "cn=config" DIT. If omitted the tool will query the remote IPA server + .TP + \fB\-n\fR, \fB\-\-no\-prompt\fR + Do not prompt for confirmation before starting migration. Use at your own risk! +-- +2.46.2 + diff --git a/0018-ipatests-Test-for-ipa-hbac-rule-duplication.patch b/0018-ipatests-Test-for-ipa-hbac-rule-duplication.patch new file mode 100644 index 0000000..7660ce0 --- /dev/null +++ b/0018-ipatests-Test-for-ipa-hbac-rule-duplication.patch @@ -0,0 +1,61 @@ +From 7f4e7e1d6a2ae9d05a2dfcf620f4df07d09d9d2b Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Thu, 3 Oct 2024 18:45:31 +0530 +Subject: [PATCH] ipatests: Test for ipa hbac rule duplication + +This test checks that ipa-migrate is not creating duplicate default hbac rules +for allow_all and allow_systemd-user rules. + +Related: https://pagure.io/freeipa/issue/9640 + +Signed-off-by: Sudhir Menon +Reviewed-By: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +--- + .../test_ipa_ipa_migration.py | 26 +++++++++++++++++++ + 1 file changed, 26 insertions(+) + +diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py +index 288165e8a83a96e6f6bd4e52866f98617f497c56..70c268951a0d7e40806742b16e62b764b2bae37b 100644 +--- a/ipatests/test_integration/test_ipa_ipa_migration.py ++++ b/ipatests/test_integration/test_ipa_ipa_migration.py +@@ -9,6 +9,7 @@ from __future__ import absolute_import + from ipatests.test_integration.base import IntegrationTest + from ipatests.pytest_ipa.integration import tasks + from ipaplatform.paths import paths ++from collections import Counter + + import pytest + import textwrap +@@ -920,3 +921,28 @@ class TestIPAMigrateScenario1(IntegrationTest): + ) + assert result.returncode == 1 + assert ERR_MSG in result.stderr_text ++ ++ def test_ipa_hbac_rule_duplication(self): ++ """ ++ This testcase checks that default hbac rules ++ are not duplicated on the local server when ++ ipa-migrate command is run. ++ """ ++ run_migrate( ++ self.replicas[0], ++ "prod-mode", ++ self.master.hostname, ++ "cn=Directory Manager", ++ self.master.config.admin_password, ++ extra_args=['-n'] ++ ) ++ result = self.replicas[0].run_command( ++ ['ipa', 'hbacrule-find'] ++ ) ++ lines = result.stdout_text.splitlines() ++ line = [] ++ for i in lines: ++ line.append(i.strip()) ++ count = Counter(line) ++ assert count.get('Rule name: allow_all') < 2 ++ assert count.get('Rule name: allow_systemd-user') < 2 +-- +2.46.2 + diff --git a/0019-ipatests-refactor-password-file-handling-in-TestHSMI.patch b/0019-ipatests-refactor-password-file-handling-in-TestHSMI.patch new file mode 100644 index 0000000..0d8eacd --- /dev/null +++ b/0019-ipatests-refactor-password-file-handling-in-TestHSMI.patch @@ -0,0 +1,116 @@ +From 142f52fc981fe9f1d693b79a7b49506af2e98829 Mon Sep 17 00:00:00 2001 +From: Mohammad Rizwan +Date: Mon, 19 Aug 2024 16:08:53 +0530 +Subject: [PATCH] ipatests: refactor password file handling in TestHSMInstall + +When token and associated certs are not being cleaned +up properly, the subsequent installation fails. Hence +Password file related scenarios moved out to new test class +so that it have fresh installation. + +Signed-off-by: Mohammad Rizwan +Reviewed-By: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +--- + .../nightly_ipa-4-12_latest.yaml | 12 ++++++++ + .../nightly_ipa-4-12_latest_selinux.yaml | 13 ++++++++ + ipatests/test_integration/test_hsm.py | 30 ++++++++++--------- + 3 files changed, 41 insertions(+), 14 deletions(-) + +diff --git a/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml +index 6d18e708fb0512ce21d8db68d4f1ab26849f40b7..07e2a8399ae4cc953adb415b975101ed20c67fd2 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml +@@ -1950,6 +1950,18 @@ jobs: + timeout: 6300 + topology: *master_3repl_1client + ++ fedora-latest-ipa-4-12/test_hsm_TestHSMInstallPasswordFile: ++ requires: [fedora-latest-ipa-4-12/build] ++ priority: 50 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-12/build_url}' ++ test_suite: test_integration/test_hsm.py::TestHSMInstallPasswordFile ++ template: *ci-ipa-4-12-latest ++ timeout: 6300 ++ topology: *master_1repl ++ + fedora-latest-ipa-4-12/test_hsm_TestHSMInstallADTrustBase: + requires: [fedora-latest-ipa-4-12/build] + priority: 50 +diff --git a/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml +index 52686df9713975c9590b8a99edb7c3442531fecc..11046be13fca1e7403d0fd74329a66ded3927a6c 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml +@@ -2105,6 +2105,19 @@ jobs: + timeout: 6300 + topology: *master_3repl_1client + ++ fedora-latest-ipa-4-12/test_hsm_TestHSMInstallPasswordFile: ++ requires: [fedora-latest-ipa-4-12/build] ++ priority: 50 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-12/build_url}' ++ selinux_enforcing: True ++ test_suite: test_integration/test_hsm.py::TestHSMInstallPasswordFile ++ template: *ci-ipa-4-12-latest ++ timeout: 6300 ++ topology: *master_1repl ++ + fedora-latest-ipa-4-12/test_hsm_TestHSMInstallADTrustBase: + requires: [fedora-latest-ipa-4-12/build] + priority: 50 +diff --git a/ipatests/test_integration/test_hsm.py b/ipatests/test_integration/test_hsm.py +index 374f5c25fd3453cd45a15d2b0f20cee424282595..42895fcd60a7c02d3b6103c2f6751a367da30b2f 100644 +--- a/ipatests/test_integration/test_hsm.py ++++ b/ipatests/test_integration/test_hsm.py +@@ -312,24 +312,26 @@ class TestHSMInstall(BaseHSMTest): + assert returncode == 0 + assert output == "No issues found." + +- def test_hsm_install_server_password_file(self): +- check_version(self.master) +- # cleanup before fresh install with password file +- for client in self.clients: +- tasks.uninstall_client(client) + +- for replica in self.replicas: +- tasks.uninstall_master(replica) ++class TestHSMInstallPasswordFile(BaseHSMTest): + +- tasks.uninstall_master(self.master) ++ num_replicas = 1 + +- delete_hsm_token([self.master] + self.replicas, self.token_name) +- self.token_name, self.token_password = get_hsm_token(self.master) +- self.master.put_file_contents(self.token_password_file, +- self.token_password) +- self.replicas[0].put_file_contents(self.token_password_file, +- self.token_password) ++ @classmethod ++ def install(cls, mh): ++ check_version(cls.master) ++ # Enable pkiuser to read softhsm tokens ++ cls.master.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods']) ++ cls.token_name, cls.token_password = get_hsm_token(cls.master) ++ cls.master.put_file_contents( ++ cls.token_password_file, cls.token_password ++ ) ++ cls.replicas[0].put_file_contents( ++ cls.token_password_file, cls.token_password ++ ) + ++ def test_hsm_install_server_password_file(self): ++ check_version(self.master) + tasks.install_master( + self.master, setup_dns=self.master_with_dns, + setup_kra=self.master_with_kra, +-- +2.46.2 + diff --git a/0020-ipatests-2FA-test-cases.patch b/0020-ipatests-2FA-test-cases.patch new file mode 100644 index 0000000..398202f --- /dev/null +++ b/0020-ipatests-2FA-test-cases.patch @@ -0,0 +1,276 @@ +From 6ac11ae003740faf19f3c75bf542ec44f717114f Mon Sep 17 00:00:00 2001 +From: Madhuri Upadhye +Date: Tue, 23 Jul 2024 18:14:36 +0530 +Subject: [PATCH] ipatests: 2FA test cases + +Added following: + +Added 'ssh_2fa_with_cmd' method for authentication, +as for '\n' with paramiko did not work. In a test case +need to just press `Enter` for `second factor`. +Advantage of above function is no having paramiko +dependancy. +We can run the any command in same session after +authentication of user. + +Test cases: +1. Authenticate the user only with password, +just press enter at `Second factor` and check tgt after auth. +when User authentication types: otp, password +2. Authenticate the user with password and otpvalues and +check tgt of user after auth when +User authentication types: otp, password + +related: https://github.com/SSSD/sssd/pull/7500 + +Signed-off-by: Madhuri Upadhye +Reviewed-By: Florence Blanc-Renaud +--- + ipatests/test_integration/test_otp.py | 192 ++++++++++++++++++++++++-- + 1 file changed, 181 insertions(+), 11 deletions(-) + +diff --git a/ipatests/test_integration/test_otp.py b/ipatests/test_integration/test_otp.py +index 878b4fb560ba8d7768ead54b065656462545babd..0babb45897c6107bf354477dbb0d3a805a3116f5 100644 +--- a/ipatests/test_integration/test_otp.py ++++ b/ipatests/test_integration/test_otp.py +@@ -5,26 +5,27 @@ + """ + import base64 + import logging +-import pytest + import re +-import time ++import tempfile + import textwrap +-from urllib.parse import urlparse, parse_qs +-from paramiko import AuthenticationException ++import time ++from urllib.parse import parse_qs, urlparse + ++import pytest + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.twofactor.hotp import HOTP + from cryptography.hazmat.primitives.twofactor.totp import TOTP +- +-from ipatests.test_integration.base import IntegrationTest +-from ipaplatform.paths import paths +-from ipatests.pytest_ipa.integration import tasks +-from ipapython.dn import DN +- + from ldap.controls.simple import BooleanControl ++from paramiko import AuthenticationException + + from ipalib import errors ++from ipaplatform.osinfo import osinfo ++from ipaplatform.paths import paths ++from ipapython.dn import DN ++from ipatests.pytest_ipa.integration import tasks ++from ipatests.test_integration.base import IntegrationTest ++from ipatests.util import xfail_context + + PASSWORD = "DummyPassword123" + USER = "opttestuser" +@@ -84,6 +85,65 @@ def kinit_otp(host, user, *, password, otp, success=True): + ) + + ++def ssh_2fa_with_cmd(host, hostname, username, password, otpvalue, ++ command="exit 0"): ++ """ ssh to user and in same session pass the command to check tgt of user ++ :param host: host to ssh ++ :param hostname: hostname to ssh ++ :param str username: The name of user ++ :param str password: password, usually the first factor ++ :param str otpvalue: generated pin of user ++ :param str command: command to execute in same session, ++ by deafult set to "exit 0" ++ :return: object class of expect command run ++ """ ++ temp_conf = tempfile.NamedTemporaryFile(suffix='.exp', delete=False) ++ with open(temp_conf.name, 'w') as tfile: ++ tfile.write('proc exitmsg { msg code } {\n') ++ tfile.write('\t# Close spawned program, if we are in the prompt\n') ++ tfile.write('\tcatch close\n\n') ++ tfile.write('\t# Wait for the exit code\n') ++ tfile.write('\tlassign [wait] pid spawnid os_error_flag rc\n\n') ++ tfile.write('\tputs ""\n') ++ tfile.write('\tputs "expect result: $msg"\n') ++ tfile.write('\tputs "expect exit code: $code"\n') ++ tfile.write('\tputs "expect spawn exit code: $rc"\n') ++ tfile.write('\texit $code\n') ++ tfile.write('}\n') ++ tfile.write('set timeout 60\n') ++ tfile.write('set prompt ".*\\[#\\$>\\] $"\n') ++ tfile.write(f'set password "{password}"\n') ++ tfile.write(f'set otpvalue "{otpvalue}"\n') ++ tfile.write(f'spawn ssh -o NumberOfPasswordPrompts=1 -o ' ++ f'StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' ++ f' -l {username} {hostname} {command}\n') ++ tfile.write('expect {\n') ++ tfile.write('"Enter first factor:*" {send -- "$password\r"}\n') ++ tfile.write('timeout {exitmsg "Unexpected output" 201}\n') ++ tfile.write('eof {exitmsg "Unexpected end of file" 202}\n') ++ tfile.write('}\n') ++ tfile.write('expect {\n') ++ tfile.write('"Enter second factor:*" {send -- "$otpvalue\r"}\n') ++ tfile.write('timeout {exitmsg "Unexpected output" 201}\n') ++ tfile.write('eof {exitmsg "Unexpected end of file" 202}\n') ++ tfile.write('}\n') ++ tfile.write('expect {\n') ++ tfile.write('"Authentication failure" ' ++ '{exitmsg "Authentication failure" 1}\n') ++ tfile.write('eof {exitmsg "Authentication successful" 0}\n') ++ tfile.write('timeout {exitmsg "Unexpected output" 201}\n') ++ tfile.write('}\n') ++ tfile.write('expect {\n') ++ tfile.write('exitmsg "Unexpected code path" 203\n') ++ tfile.write('EOF\n') ++ tfile.write('}') ++ host.transport.put_file(temp_conf.name, '/tmp/ssh.exp') ++ tasks.clear_sssd_cache(host) ++ expect_cmd = 'expect -f /tmp/ssh.exp' ++ cmd = host.run_command(expect_cmd, raiseonerr=False) ++ return cmd ++ ++ + def ssh_2f(hostname, username, answers_dict, port=22, unwanted_prompt=""): + """ + :param hostname: hostname +@@ -91,6 +151,7 @@ def ssh_2f(hostname, username, answers_dict, port=22, unwanted_prompt=""): + :param answers_dict: dictionary of options with prompt_message and value. + :param port: port for ssh + """ ++ + # Handler for server questions + def answer_handler(title, instructions, prompt_list): + resp = [] +@@ -131,8 +192,9 @@ class TestOTPToken(IntegrationTest): + + @classmethod + def install(cls, mh): +- super(TestOTPToken, cls).install(mh) + master = cls.master ++ tasks.install_packages(master, ['expect']) ++ super(TestOTPToken, cls).install(mh) + + tasks.kinit_admin(master) + # create service with OTP auth indicator +@@ -398,6 +460,114 @@ class TestOTPToken(IntegrationTest): + self.master.run_command(['semanage', 'login', '-D']) + sssd_conf_backup.restore() + ++ def test_2fa_only_with_password(self): ++ """Test ssh with 2FA only with the password(first factor) when ++ user-auth-type is opt and password. ++ ++ Test for : https://github.com/SSSD/sssd/pull/7500 ++ ++ Add the IPA user and user-auth-type set to opt and password. ++ Authenticate the user only with password, just press enter ++ at `Second factor` ++ """ ++ master = self.master ++ USER3 = 'sshuser3' ++ sssd_conf_backup = tasks.FileBackup(master, paths.SSSD_CONF) ++ first_prompt = 'Enter first factor:' ++ second_prompt = 'Enter second factor:' ++ add_contents = textwrap.dedent(''' ++ [prompting/2fa/sshd] ++ single_prompt = False ++ first_prompt = {0} ++ second_prompt = {1} ++ ''').format(first_prompt, second_prompt) ++ set_sssd_conf(master, add_contents) ++ tasks.create_active_user(master, USER3, PASSWORD) ++ tasks.kinit_admin(master) ++ master.run_command(['ipa', 'user-mod', USER3, '--user-auth-type=otp', ++ '--user-auth-type=password']) ++ try: ++ otpuid, totp = add_otptoken(master, USER3, otptype='totp') ++ master.run_command(['ipa', 'otptoken-show', otpuid]) ++ totp.generate(int(time.time())).decode('ascii') ++ otpvalue = "\n" ++ tasks.clear_sssd_cache(self.master) ++ github_ticket = "https://github.com/SSSD/sssd/pull/7500" ++ sssd_version = tasks.get_sssd_version(master) ++ rhel_fail = ( ++ osinfo.id == 'rhel' ++ and sssd_version < tasks.parse_version("2.9.5") ++ ) ++ fedora_fail = ( ++ osinfo.id == 'fedora' ++ and sssd_version == tasks.parse_version("2.9.5") ++ ) ++ with xfail_context(rhel_fail or fedora_fail, reason=github_ticket): ++ result = ssh_2fa_with_cmd(master, ++ self.master.external_hostname, ++ USER3, PASSWORD, otpvalue=otpvalue, ++ command="klist") ++ print(result.stdout_text) ++ assert ('Authentication successful') in result.stdout_text ++ assert USER3 in result.stdout_text ++ assert (f'Default principal: ' ++ f'{USER3}@{self.master.domain.realm}' in ++ result.stdout_text) ++ cmd = self.master.run_command(['semanage', 'login', '-l']) ++ assert USER3 in cmd.stdout_text ++ finally: ++ master.run_command(['ipa', 'user-del', USER3]) ++ self.master.run_command(['semanage', 'login', '-D']) ++ sssd_conf_backup.restore() ++ ++ def test_2fa_with_otp_password(self): ++ """Test ssh with 2FA only with password and otpvalue when ++ user-auth-type is opt and password. ++ ++ Test for : https://github.com/SSSD/sssd/pull/7500 ++ ++ Add the IPA user and user-auth-type set to opt and password. ++ Authenticate the user only with password and otpvalue. ++ """ ++ master = self.master ++ USER4 = 'sshuser4' ++ sssd_conf_backup = tasks.FileBackup(master, paths.SSSD_CONF) ++ first_prompt = 'Enter first factor:' ++ second_prompt = 'Enter second factor:' ++ add_contents = textwrap.dedent(''' ++ [prompting/2fa/sshd] ++ single_prompt = False ++ first_prompt = {0} ++ second_prompt = {1} ++ ''').format(first_prompt, second_prompt) ++ set_sssd_conf(master, add_contents) ++ tasks.create_active_user(master, USER4, PASSWORD) ++ tasks.kinit_admin(master) ++ ++ master.run_command(['ipa', 'user-mod', USER4, '--user-auth-type=otp', ++ '--user-auth-type=password']) ++ try: ++ otpuid, totp = add_otptoken(master, USER4, otptype='totp') ++ master.run_command(['ipa', 'otptoken-show', otpuid]) ++ otpvalue = totp.generate(int(time.time())).decode('ascii') ++ tasks.clear_sssd_cache(self.master) ++ result = ssh_2fa_with_cmd(master, ++ self.master.external_hostname, ++ USER4, PASSWORD, otpvalue=otpvalue, ++ command="klist") ++ print(result.stdout_text) ++ cmd = self.master.run_command(['semanage', 'login', '-l']) ++ # check the output ++ assert ('Authentication successful') in result.stdout_text ++ assert USER4 in result.stdout_text ++ assert (f'Default principal: {USER4}@' ++ f'{self.master.domain.realm}' in result.stdout_text) ++ assert USER4 in cmd.stdout_text ++ finally: ++ master.run_command(['ipa', 'user-del', USER4]) ++ self.master.run_command(['semanage', 'login', '-D']) ++ sssd_conf_backup.restore() ++ + @pytest.fixture + def setup_otp_nsslapd(self): + check_services = self.master.run_command( +-- +2.46.2 + diff --git a/freeipa.spec b/freeipa.spec index f055694..c1bca9d 100644 --- a/freeipa.spec +++ b/freeipa.spec @@ -70,7 +70,7 @@ %global krb5_kdb_version 9.0 # 0.7.16: https://github.com/drkjam/netaddr/issues/71 %global python_netaddr_version 0.7.19 -%global samba_version 4.20.0-103 +%global samba_version 4.21.1 %global slapi_nis_version 0.56.4 %global python_ldap_version 3.1.0-1 %if 0%{?rhel} < 9 @@ -88,7 +88,7 @@ %global bind_version 9.11.20-6 # support for passkey -%global sssd_version 2.9.0 +%global sssd_version 2.9.5 %else # Fedora @@ -224,7 +224,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 1%{?rc_version:.%rc_version}%{?dist} +Release: 2%{?rc_version:.%rc_version}%{?dist} Summary: The Identity, Policy and Audit system License: GPL-3.0-or-later @@ -250,6 +250,24 @@ Patch1002: 1002-Revert-freeipa.spec-depend-on-bind-dnssec-utils.patch %if 0%{?rhel} == 9 Patch0001: 0001-Revert-Replace-netifaces-with-ifaddr.patch Patch0002: 0002-Revert-custodia-do-not-use-deprecated-jwcrypto-wrapp.patch +Patch0003: 0003-ipatests-Check-Default-PAC-type-is-added-to-config.patch +Patch0004: 0004-selinux-add-all-IPA-log-files-to-ipa_log_t-file-cont.patch +Patch0005: 0005-Add-ipa-idrange-fix.patch +Patch0006: 0006-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch +Patch0007: 0007-ipatests-Update-ipa-adtrust-install-test.patch +Patch0008: 0008-Installer-activate-ssh-service-in-sssd.conf.patch +Patch0009: 0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch +Patch0010: 0010-ipa-migrate-fix-alternate-entry-search-filter.patch +Patch0011: 0011-ipatests-provide-a-ccache-to-rpcclient-deletetrustdo.patch +Patch0012: 0012-test_adtrust_install-add-use-krb5-ccache-to-smbclien.patch +Patch0013: 0013-Don-t-rely-on-removing-the-CA-to-uninstall-the-ACME-.patch +Patch0014: 0014-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch +Patch0015: 0015-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch +Patch0016: 0016-ipatests-Activate-ssh-in-sssd.conf.patch +Patch0017: 0017-ipa-migrate-man-page-fix-typos-and-errors.patch +Patch0018: 0018-ipatests-Test-for-ipa-hbac-rule-duplication.patch +Patch0019: 0019-ipatests-refactor-password-file-handling-in-TestHSMI.patch +Patch0020: 0020-ipatests-2FA-test-cases.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch %endif %endif @@ -1496,6 +1514,7 @@ fi %{_sbindir}/ipa-pkinit-manage %{_sbindir}/ipa-crlgen-manage %{_sbindir}/ipa-cert-fix +%{_sbindir}/ipa-idrange-fix %{_sbindir}/ipa-acme-manage %{_sbindir}/ipa-migrate %if 0%{?fedora} >= 38 @@ -1575,6 +1594,7 @@ fi %{_mandir}/man1/ipa-pkinit-manage.1* %{_mandir}/man1/ipa-crlgen-manage.1* %{_mandir}/man1/ipa-cert-fix.1* +%{_mandir}/man1/ipa-idrange-fix.1* %{_mandir}/man1/ipa-acme-manage.1* %{_mandir}/man1/ipa-migrate.1* @@ -1863,6 +1883,16 @@ fi %endif %changelog +* Mon Oct 21 2024 Florence Blanc-Renaud - 4.12.2-2 +- Related: RHEL-59788 Rebase Samba to the latest 4.21.x release +- Fixes: RHEL-61642 Uninstall ACME separately during PKI uninstallation +- Fixes: RHEL-56963 SSSD offline causing test-adtrust-install failure +- Fixes: RHEL-56473 Include latest fixes in python3-ipatests packages +- Fixes: RHEL-48104 Default hbac rules are duplicated on remote server post ipa-migrate in prod-mode +- Fixes: RHEL-45330 [RFE] add a tool to quickly detect and fix issues with IPA ID ranges +- Fixes: RHEL-40376 SID generation task is failing when SELinux is in Enforcing mode +- Fixes: RHEL-4915 Last expired OTP token would be considered as still assigned to the user + * Wed Aug 21 2024 Florence Blanc-Renaud - 4.12.2-1 - Resolves: RHEL-54546 Covscan issues: Resource Leak - Resolves: RHEL-49602 misleading warning for missing ipa-selinux-nfast package on luna hsm h/w