mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-26 17:01:14 -06:00
1e6a204b43
Some API contexts are used to modify global state (e.g. files in /etc and /var). These contexts do not support confdir overrides. Initialize the API with an explicit confdir argument to paths.ETC_IPA. The special contexts are: * backup * cli_installer * installer * ipctl * renew * restore * server * updates The patch also corrects the context of the ipa-httpd-kdcproxy script to 'server'. https://fedorahosted.org/freeipa/ticket/6389 Signed-off-by: Christian Heimes <cheimes@redhat.com> Reviewed-By: Jan Cholasta <jcholast@redhat.com>
383 lines
14 KiB
Python
383 lines
14 KiB
Python
# Authors: Tomas Babej <tbabej@redhat.com>
|
|
#
|
|
# Copyright (C) 2015 Red Hat
|
|
# see file 'COPYING' for use and warranty information
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
import gssapi
|
|
import sys
|
|
|
|
import six
|
|
|
|
from ipalib import api
|
|
from ipalib import errors
|
|
from ipaplatform.paths import paths
|
|
from ipapython import admintool
|
|
from ipapython.dn import DN
|
|
from ipapython.ipautil import realm_to_suffix, posixify
|
|
from ipaserver.install import replication, installutils
|
|
|
|
if six.PY3:
|
|
unicode = str
|
|
|
|
DEFAULT_TRUST_VIEW_NAME = u'Default Trust View'
|
|
|
|
|
|
class WinsyncMigrate(admintool.AdminTool):
|
|
"""
|
|
Tool to migrate winsync users.
|
|
"""
|
|
|
|
command_name = 'ipa-winsync-migrate'
|
|
usage = "ipa-winsync-migrate"
|
|
description = (
|
|
"This tool creates user ID overrides for all the users "
|
|
"that were previously synced from AD domain using the "
|
|
"winsync replication agreement. It requires that trust "
|
|
"with the AD forest has already been established and "
|
|
"the users in question are resolvable using SSSD. "
|
|
"For more information, see `man ipa-winsync-migrate`."
|
|
)
|
|
|
|
@classmethod
|
|
def add_options(cls, parser):
|
|
"""
|
|
Adds command line options to the tool.
|
|
"""
|
|
super(WinsyncMigrate, cls).add_options(parser)
|
|
|
|
parser.add_option(
|
|
"--realm",
|
|
dest="realm",
|
|
help="The AD realm the winsynced users belong to")
|
|
parser.add_option(
|
|
"--server",
|
|
dest="server",
|
|
help="The AD DC the winsync agreement is established with")
|
|
parser.add_option(
|
|
"-U", "--unattended",
|
|
dest="interactive",
|
|
action="store_false",
|
|
default=True,
|
|
help="Never prompt for user input")
|
|
|
|
def validate_options(self):
|
|
"""
|
|
Validates the options passed by the user:
|
|
- Checks that trust has been established with
|
|
the realm passed via --realm option
|
|
"""
|
|
|
|
super(WinsyncMigrate, self).validate_options(needs_root=True)
|
|
|
|
if self.options.realm is None:
|
|
raise admintool.ScriptError(
|
|
"AD realm the winsynced users belong to needs to be "
|
|
"specified.")
|
|
else:
|
|
try:
|
|
api.Command['trust_show'](unicode(self.options.realm))
|
|
except errors.NotFound:
|
|
raise admintool.ScriptError(
|
|
"Trust with the given realm %s could not be found. "
|
|
"Please establish the trust prior to migration."
|
|
% self.options.realm)
|
|
except Exception as e:
|
|
raise admintool.ScriptError(
|
|
"An error occured during detection of the established "
|
|
"trust with %s: %s" % (self.options.realm, str(e)))
|
|
|
|
if self.options.server is None:
|
|
raise admintool.ScriptError(
|
|
"The AD DC the winsync agreement is established with "
|
|
"needs to be specified.")
|
|
else:
|
|
# Validate the replication agreement between given host and localhost
|
|
try:
|
|
manager = replication.ReplicationManager(
|
|
api.env.realm,
|
|
api.env.host,
|
|
None) # Use GSSAPI instead of raw directory manager access
|
|
|
|
replica_type = manager.get_agreement_type(self.options.server)
|
|
except errors.ACIError as e:
|
|
raise admintool.ScriptError(
|
|
"Used Kerberos account does not have privileges to access "
|
|
"the replication agreement info: %s" % str(e))
|
|
except errors.NotFound as e:
|
|
raise admintool.ScriptError(
|
|
"The replication agreement between %s and %s could not "
|
|
"be detected" % (api.env.host, self.options.server))
|
|
|
|
# Check that the replication agreement is indeed WINSYNC
|
|
if replica_type != replication.WINSYNC:
|
|
raise admintool.ScriptError(
|
|
"Replication agreement between %s and %s is not winsync."
|
|
% (api.env.host, self.options.server))
|
|
|
|
# Save the reference to the replication manager in the object
|
|
self.manager = manager
|
|
|
|
def delete_winsync_agreement(self):
|
|
"""
|
|
Deletes the winsync agreement between the current master and the
|
|
given AD server.
|
|
"""
|
|
|
|
try:
|
|
self.manager.delete_agreement(self.options.server)
|
|
self.manager.delete_referral(self.options.server)
|
|
|
|
dn = DN(('cn', self.options.server),
|
|
('cn', 'replicas'),
|
|
('cn', 'ipa'),
|
|
('cn', 'etc'),
|
|
realm_to_suffix(api.env.realm))
|
|
entries = self.manager.conn.get_entries(dn,
|
|
self.ldap.SCOPE_SUBTREE)
|
|
if entries:
|
|
entries.sort(key=len, reverse=True)
|
|
for entry in entries:
|
|
self.ldap.delete_entry(entry)
|
|
|
|
except Exception as e:
|
|
raise admintool.ScriptError(
|
|
"Deletion of the winsync agreement failed: %s" % str(e))
|
|
|
|
|
|
def create_id_user_override(self, entry):
|
|
"""
|
|
Creates ID override corresponding to this user entry.
|
|
"""
|
|
|
|
user_identifier = u"%s@%s" % (entry['uid'][0], self.options.realm)
|
|
|
|
kwargs = {
|
|
'uid': entry['uid'][0],
|
|
'uidnumber': entry['uidnumber'][0],
|
|
'gidnumber': entry['gidnumber'][0],
|
|
'gecos': entry['gecos'][0],
|
|
'loginshell': entry['loginshell'][0]
|
|
}
|
|
|
|
try:
|
|
api.Command['idoverrideuser_add'](
|
|
DEFAULT_TRUST_VIEW_NAME,
|
|
user_identifier,
|
|
**kwargs
|
|
)
|
|
except Exception as e:
|
|
self.log.warning("Migration failed: %s (%s)"
|
|
% (user_identifier, str(e)))
|
|
else:
|
|
self.log.debug("Migrated: %s" % user_identifier)
|
|
|
|
def find_winsync_users(self):
|
|
"""
|
|
Finds all users that were mirrored from AD using winsync.
|
|
"""
|
|
|
|
user_filter = "(&(objectclass=ntuser)(ntUserDomainId=*))"
|
|
user_base = DN(api.env.container_user, api.env.basedn)
|
|
entries, _truncated = self.ldap.find_entries(
|
|
filter=user_filter,
|
|
base_dn=user_base,
|
|
paged_search=True)
|
|
|
|
for entry in entries:
|
|
self.log.debug("Discovered entry: %s" % entry)
|
|
|
|
return entries
|
|
|
|
def migrate_memberships(self, user_entry, winsync_group_prefix,
|
|
object_membership_command,
|
|
object_info_command,
|
|
user_dn_attribute,
|
|
object_group_membership_key,
|
|
object_container_dn):
|
|
"""
|
|
Migrates user memberships to theier external identities.
|
|
|
|
All migrated users for the given object are migrated to a common
|
|
external group which is then assigned to the given object as a
|
|
(user) member group.
|
|
"""
|
|
|
|
def winsync_group_name(object_entry):
|
|
"""
|
|
Returns the generated name of group containing migrated external
|
|
users.
|
|
|
|
The group name is of the form:
|
|
"<prefix>_<object name>_winsync_external"
|
|
|
|
Object name is converted to posix-friendly string by omitting
|
|
and/or replacing characters. This may lead to collisions, i.e.
|
|
if both 'trust_admins' and 'trust admin' groups have winsync
|
|
users being migrated.
|
|
"""
|
|
|
|
return u"{0}_{1}_winsync_external".format(
|
|
winsync_group_prefix,
|
|
posixify(object_entry['cn'][0])
|
|
)
|
|
|
|
def create_winsync_group(object_entry, suffix=0):
|
|
"""
|
|
Creates the group containing migrated external users that were
|
|
previously available via winsync.
|
|
"""
|
|
|
|
name = winsync_group_name(object_entry)
|
|
|
|
# Only non-trivial suffix is appended at the end
|
|
if suffix != 0:
|
|
name += str(suffix)
|
|
|
|
try:
|
|
api.Command['group_add'](name, external=True)
|
|
except errors.DuplicateEntry:
|
|
# If there is a collision, let's try again with a higher suffix
|
|
create_winsync_group(object_entry, suffix=suffix+1)
|
|
else:
|
|
# In case of no collision, add the membership
|
|
api.Command[object_membership_command](object_entry['cn'][0], group=[name])
|
|
|
|
# Search for all objects containing the given user as a direct member
|
|
member_filter = self.ldap.make_filter_from_attr(user_dn_attribute,
|
|
user_entry.dn)
|
|
|
|
try:
|
|
objects, _truncated = self.ldap.find_entries(
|
|
member_filter,
|
|
base_dn=object_container_dn)
|
|
except errors.EmptyResult:
|
|
# If there's nothing to migrate, then let's get out of here
|
|
return
|
|
|
|
# The external user cannot be added directly as member of the IPA
|
|
# objects, hence we need to wrap all the external users into one
|
|
# new external group, which will be then added to the original IPA
|
|
# object as a member.
|
|
|
|
for obj in objects:
|
|
# Check for existence of winsync external group
|
|
name = winsync_group_name(obj)
|
|
info = api.Command[object_info_command](obj['cn'][0])['result']
|
|
|
|
# If it was not created yet, do it now
|
|
if name not in info.get(object_group_membership_key, []):
|
|
create_winsync_group(obj)
|
|
|
|
# Add the user to the external group. Membership is migrated
|
|
# at this point.
|
|
user_identifier = u"%s@%s" % (user_entry['uid'][0], self.options.realm)
|
|
api.Command['group_add_member'](name, ipaexternalmember=[user_identifier])
|
|
|
|
def migrate_group_memberships(self, user_entry):
|
|
return self.migrate_memberships(user_entry,
|
|
winsync_group_prefix="group",
|
|
user_dn_attribute="member",
|
|
object_membership_command="group_add_member",
|
|
object_info_command="group_show",
|
|
object_group_membership_key="member_group",
|
|
object_container_dn=DN(api.env.container_group, api.env.basedn),
|
|
)
|
|
|
|
def migrate_role_memberships(self, user_entry):
|
|
return self.migrate_memberships(user_entry,
|
|
winsync_group_prefix="role",
|
|
user_dn_attribute="member",
|
|
object_membership_command="role_add_member",
|
|
object_info_command="role_show",
|
|
object_group_membership_key="member_group",
|
|
object_container_dn=DN(api.env.container_rolegroup, api.env.basedn),
|
|
)
|
|
|
|
def migrate_hbac_memberships(self, user_entry):
|
|
return self.migrate_memberships(user_entry,
|
|
winsync_group_prefix="hbacrule",
|
|
user_dn_attribute="memberuser",
|
|
object_membership_command="hbacrule_add_user",
|
|
object_info_command="hbacrule_show",
|
|
object_group_membership_key="memberuser_group",
|
|
object_container_dn=DN(api.env.container_hbac, api.env.basedn),
|
|
)
|
|
|
|
def migrate_selinux_memberships(self, user_entry):
|
|
return self.migrate_memberships(user_entry,
|
|
winsync_group_prefix="selinux",
|
|
user_dn_attribute="memberuser",
|
|
object_membership_command="selinuxusermap_add_user",
|
|
object_info_command="selinuxusermap_show",
|
|
object_group_membership_key="memberuser_group",
|
|
object_container_dn=DN(api.env.container_selinux, api.env.basedn),
|
|
)
|
|
|
|
def warn_passsync(self):
|
|
self.log.warning("Migration completed. Please note that if PassSync "
|
|
"was configured on the given Active Directory server, "
|
|
"it needs to be manually removed, otherwise it may try "
|
|
"to reset password for accounts that are no longer existent.")
|
|
|
|
@classmethod
|
|
def main(cls, argv):
|
|
"""
|
|
Sets up API and LDAP connection for the tool, then runs the rest of
|
|
the plumbing.
|
|
"""
|
|
|
|
# Check if the IPA server is configured before attempting to migrate
|
|
try:
|
|
installutils.check_server_configuration()
|
|
except RuntimeError as e:
|
|
sys.exit(e)
|
|
|
|
# Finalize API
|
|
api.bootstrap(in_server=True, context='server', confdir=paths.ETC_IPA)
|
|
api.finalize()
|
|
|
|
# Setup LDAP connection
|
|
try:
|
|
api.Backend.ldap2.connect()
|
|
cls.ldap = api.Backend.ldap2
|
|
except gssapi.exceptions.GSSError as e:
|
|
sys.exit("Must have Kerberos credentials to migrate Winsync users. Error: %s" % e)
|
|
except errors.ACIError as e:
|
|
sys.exit("Outdated Kerberos credentials. Use kdestroy and kinit to update your ticket.")
|
|
except errors.DatabaseError as e:
|
|
sys.exit("Cannot connect to the LDAP database. Please check if IPA is running.")
|
|
|
|
super(WinsyncMigrate, cls).main(argv)
|
|
|
|
def run(self):
|
|
super(WinsyncMigrate, self).run()
|
|
|
|
# Stop winsync agreement with the given host
|
|
self.delete_winsync_agreement()
|
|
|
|
# Create ID overrides replacing the user winsync entries
|
|
entries = self.find_winsync_users()
|
|
for entry in entries:
|
|
self.create_id_user_override(entry)
|
|
self.migrate_group_memberships(entry)
|
|
self.migrate_role_memberships(entry)
|
|
self.migrate_hbac_memberships(entry)
|
|
self.migrate_selinux_memberships(entry)
|
|
self.ldap.delete_entry(entry)
|
|
|
|
self.warn_passsync()
|