ipa/0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch

405 lines
15 KiB
Diff
Raw Normal View History

From 8d242ba741ec22b258d5e70a530cefd0940783c7 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
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 <mreynolds@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipaserver/install/ipa_migrate.py | 119 +++++++++++++++++++--
ipaserver/install/ipa_migrate_constants.py | 82 +++++++++++++--
2 files changed, 187 insertions(+), 14 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, 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"
#
@@ -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.0