# Authors: Tomas Babej # # 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 . # import krbV import sys from ipalib import api from ipalib import errors from ipapython import admintool from ipapython.dn import DN from ipapython.ipautil import realm_to_suffix from ipapython.ipa_log_manager import log_mgr from ipaserver.plugins.ldap2 import ldap2 from ipaserver.install import replication DEFAULT_TRUST_VIEW_NAME = u'Default Trust View' class WinsyncMigrate(admintool.AdminTool): """ Tool to migrate winsync users. """ command_name = 'ipa-migrate-winsync' usage = "ipa-migrate-winsync" 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-migrate-winsync`." ) @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: result = 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, _ = 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 """ return u"{0}_{1}_winsync_external".format( winsync_group_prefix, object_entry['cn'][0] ) def create_winsync_group(object_entry): """ Creates the group containing migrated external users that were previously available via winsync. """ name = winsync_group_name(object_entry) api.Command['group_add'](name, external=True) 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, _ = 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), ) @classmethod def main(cls, argv): """ Sets up API and LDAP connection for the tool, then runs the rest of the plumbing. """ # Finalize API api.bootstrap(in_server=True, context='server') api.finalize() # Setup LDAP connection try: ctx = krbV.default_context() ccache = ctx.default_ccache() api.Backend.ldap2.connect(ccache) cls.ldap = api.Backend.ldap2 except krbV.Krb5Error, e: sys.exit("Must have Kerberos credentials to migrate Winsync users.") except errors.ACIError, e: sys.exit("Outdated Kerberos credentials. Use kdestroy and kinit to update your ticket.") except errors.DatabaseError, 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)