405 lines
15 KiB
Diff
405 lines
15 KiB
Diff
|
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
|
||
|
|