summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--install/Makefile.am1
-rw-r--r--install/conf/ipa.conf12
-rw-r--r--install/configure.ac1
-rw-r--r--install/migration/Makefile.am18
-rw-r--r--install/migration/error.html21
-rw-r--r--install/migration/index.html47
-rw-r--r--install/migration/invalid.html21
-rw-r--r--install/migration/migration.css69
-rw-r--r--install/migration/migration.py67
-rw-r--r--ipa.spec.in6
-rw-r--r--ipalib/plugins/migration.py374
11 files changed, 637 insertions, 0 deletions
diff --git a/install/Makefile.am b/install/Makefile.am
index 19665856e..68a3c2655 100644
--- a/install/Makefile.am
+++ b/install/Makefile.am
@@ -7,6 +7,7 @@ NULL =
SUBDIRS = \
conf \
html \
+ migration \
share \
tools \
updates \
diff --git a/install/conf/ipa.conf b/install/conf/ipa.conf
index 81a6bc695..b9562936f 100644
--- a/install/conf/ipa.conf
+++ b/install/conf/ipa.conf
@@ -100,6 +100,18 @@ Alias /ipa-assets/ "/var/cache/ipa/assets/"
ErrorDocument 401 /ipa/errors/unauthorized.html
</Directory>
+# migration related pages
+Alias /ipa/migration "/usr/share/ipa/migration"
+
+<Directory "/usr/share/ipa/migration">
+ AllowOverride None
+ Satisfy Any
+ Allow from all
+
+ AddHandler mod_python .py
+ PythonHandler mod_python.publisher
+</Directory>
+
#Alias /ipatest "/usr/share/ipa/ipatest"
#<Directory "/usr/share/ipa/ipatest">
diff --git a/install/configure.ac b/install/configure.ac
index 7f96812f0..826eeb047 100644
--- a/install/configure.ac
+++ b/install/configure.ac
@@ -34,6 +34,7 @@ AC_CONFIG_FILES([
Makefile
conf/Makefile
html/Makefile
+ migration/Makefile
share/Makefile
tools/Makefile
tools/man/Makefile
diff --git a/install/migration/Makefile.am b/install/migration/Makefile.am
new file mode 100644
index 000000000..201a807cd
--- /dev/null
+++ b/install/migration/Makefile.am
@@ -0,0 +1,18 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/migration
+app_DATA = \
+ error.html \
+ index.html \
+ invalid.html \
+ migration.css \
+ migration.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/install/migration/error.html b/install/migration/error.html
new file mode 100644
index 000000000..93ca8d294
--- /dev/null
+++ b/install/migration/error.html
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <link rel="stylesheet" href="migration.css" type="text/css">
+ <title>IPA Password Migration Page: Error</title>
+</head>
+
+<body>
+<p>
+There was a problem with your request. Please, try again later.
+</p>
+<p>
+If the problem persists, contact your administrator.
+</p>
+</body>
+
diff --git a/install/migration/index.html b/install/migration/index.html
new file mode 100644
index 000000000..b3ea46b2f
--- /dev/null
+++ b/install/migration/index.html
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <link rel="stylesheet" href="migration.css" type="text/css">
+ <title>IPA Password Migration Page</title>
+</head>
+
+<body>
+<p>
+If you have been sent here by your administrator, your personal
+information is being migrated to a new Identity management solution (IPA).
+</p>
+<p>
+Please, enter your credentials in the form below to complete the process.
+</p>
+<p>
+Upon successful login your Kerberos account will be activated.
+</p>
+<div class="migration_form">
+<div class="migration_form_inner">
+<form action="migration.py/bind" method="post">
+ <div class="migration_form_title">
+ <span>Password Migration</span>
+ </div>
+ <div class="migration_form_input">
+ <label><em>U</em>sername:</label>
+ <input name="username" value="" type="text" accesskey="u" />
+ </div>
+ <div class="migration_form_input">
+ <label><em>P</em>assword:</label>
+ <input name="password" value="" type="password" accesskey="p" />
+ </div>
+ <div class="migration_form_submit">
+ <input name="submit" value="Migrate!" type="submit" />
+ </div>
+</form>
+</div>
+</div>
+</body>
+
+</html>
+
diff --git a/install/migration/invalid.html b/install/migration/invalid.html
new file mode 100644
index 000000000..70aa90daf
--- /dev/null
+++ b/install/migration/invalid.html
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <link rel="stylesheet" href="migration.css" type="text/css">
+ <title>IPA Password Migration Page: Invalid credentials</title>
+</head>
+
+<body>
+<p>
+Invalid username or password.
+</p>
+<p>
+<a href="index.html">Let me try again!</a>
+</p>
+</body>
+
diff --git a/install/migration/migration.css b/install/migration/migration.css
new file mode 100644
index 000000000..c32b1525c
--- /dev/null
+++ b/install/migration/migration.css
@@ -0,0 +1,69 @@
+/* migration page CSS; author: Pavel Zuna <pzuna@redhat.com> */
+
+body
+{
+ font-family: Verdana;
+ text-align: center;
+}
+
+p
+{
+ font-size: 0.8em;
+ font-weight: bold;
+}
+
+.migration_form
+{
+ margin-left: auto;
+ margin-right: auto;
+ text-align: center;
+ width: 18em;
+}
+
+.migration_form_inner
+{
+ border: solid 1px #284775;
+ font-size: 0.8em;
+ padding: 4px;
+}
+
+.migration_form_title
+{
+ background: #5d7b9d;
+ color: #f7f6f3;
+ font-weight: bold;
+ height: 1.7em;
+ margin-bottom: 0.3em;
+ padding-top: 0.4em;
+ text-align: center;
+}
+
+.migration_form_input
+{
+ color: #5d7b9d;
+ font-size: 1em;
+ text-align: right;
+}
+
+.migration_form_input em
+{
+ font-style: normal;
+ text-decoration: underline;
+}
+
+.migration_form_submit
+{
+ text-align: center;
+}
+
+.migration_form_submit input
+{
+ background: #5d7b9d;
+ border: solid 1px #284775;
+ color: #f7f6f3;
+ height: 1.7em;
+ margin-top: 0.3em;
+}
+
+/* end of file */
+
diff --git a/install/migration/migration.py b/install/migration/migration.py
new file mode 100644
index 000000000..bf12c5cec
--- /dev/null
+++ b/install/migration/migration.py
@@ -0,0 +1,67 @@
+# Authors:
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2009 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+Password migration script
+"""
+
+import ldap
+from mod_python import apache, util
+
+
+BASE_DN = ''
+LDAP_URI = 'ldap://localhost:389'
+
+
+def get_base_dn():
+ """
+ Retrieve LDAP server base DN.
+ """
+ if BASE_DN:
+ return BASE_DN
+ try:
+ conn = ldap.initialize(LDAP_URI)
+ conn.simple_bind_s('', '')
+ entries = conn.search_ext_s(
+ '', scope=ldap.SCOPE_BASE, attrlist=['namingcontexts']
+ )
+ except ldap.LDAPError:
+ return ''
+ conn.unbind_s()
+ try:
+ return entries[0][1]['namingcontexts'][0]
+ except (IndexError, KeyError):
+ return ''
+
+
+def bind(req, username, password):
+ base_dn = get_base_dn()
+ if not base_dn:
+ util.redirect(req, '/ipa/migration/error.html')
+ bind_dn = 'uid=%s,cn=users,cn=accounts,%s' % (username, base_dn)
+ try:
+ conn = ldap.initialize(LDAP_URI)
+ conn.simple_bind_s(bind_dn, password)
+ except (ldap.INVALID_CREDENTIALS, ldap.UNWILLING_TO_PERFORM,
+ ldap.NO_SUCH_OBJECT):
+ util.redirect(req, '/ipa/migration/invalid.html')
+ except ldap.LDAPError:
+ util.redirect(req, '/ipa/migration/error.html')
+ conn.unbind_s()
+ util.redirect(req, '/ipa/ui')
+
diff --git a/ipa.spec.in b/ipa.spec.in
index 6b5e655c6..5f792e101 100644
--- a/ipa.spec.in
+++ b/ipa.spec.in
@@ -382,6 +382,12 @@ fi
%dir %{_usr}/share/ipa/html
%{_usr}/share/ipa/html/ssbrowser.html
%{_usr}/share/ipa/html/unauthorized.html
+%dir %{_usr}/share/ipa/migration
+%{_usr}/share/ipa/migration/error.html
+%{_usr}/share/ipa/migration/index.html
+%{_usr}/share/ipa/migration/invalid.html
+%{_usr}/share/ipa/migration/migration.css
+%{_usr}/share/ipa/migration/migration.py*
%dir %{_sysconfdir}/ipa
%dir %{_sysconfdir}/ipa/html
%config(noreplace) %{_sysconfdir}/ipa/html/ssbrowser.html
diff --git a/ipalib/plugins/migration.py b/ipalib/plugins/migration.py
new file mode 100644
index 000000000..a0ff94b3e
--- /dev/null
+++ b/ipalib/plugins/migration.py
@@ -0,0 +1,374 @@
+# Authors:
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2009 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+Migration to IPA
+
+Example: Migrate users and groups from DS to IPA
+
+ ipa migrate-ds ldap://example.com:389
+"""
+
+import logging
+import re
+
+from ipalib import api, errors, output, uuid
+from ipalib import Command, List, Password, Str
+from ipalib.cli import to_cli
+from ipaserver.plugins.ldap2 import ldap2
+
+
+# USER MIGRATION CALLBACKS AND VARS
+
+_krb_err_msg = 'Kerberos principal %s already exists. ' \
+ 'Use \'ipa user-mod\' to set it manually.'
+_grp_err_msg = 'Failed to add user to the default group. ' \
+ 'Use \'ipa group-add-member\' to add manually.'
+
+
+def _pre_migrate_user(ldap, pkey, dn, entry_attrs, failed, config, ctx):
+ # get default primary group for new users
+ if 'def_group_dn' not in ctx:
+ def_group = config.get('ipadefaultprimarygroup')
+ ctx['def_group_dn'] = api.Object.group.get_dn(def_group)
+ try:
+ (g_dn, g_attrs) = ldap.get_entry(ctx['def_group_dn'], ['gidnumber'])
+ except errors.NotFound:
+ error_msg = 'Default group for new users not found.'
+ raise errors.NotFound(reason=error_msg)
+ ctx['def_group_gid'] = g_attrs['gidnumber'][0]
+
+ # fill in required attributes by IPA
+ entry_attrs['ipauniqueid'] = str(uuid.uuid1())
+ if 'homedirectory' not in entry_attrs:
+ homes_root = config.get('ipahomesrootdir', ('/home', ))[0]
+ home_dir = '%s/%s' % (homes_root, pkey)
+ home_dir = home_dir.replace('//', '/').rstrip('/')
+ entry_attrs['homedirectory'] = home_dir
+ entry_attrs.setdefault('gidnumber', ctx['def_group_gid'])
+
+ # generate a principal name and check if it isn't already taken
+ principal = '%s@%s' % (pkey, api.env.realm)
+ try:
+ ldap.find_entry_by_attr(
+ 'krbprincipalname', principal, 'krbprincipalaux', ['']
+ )
+ except errors.NotFound:
+ entry_attrs['krbprincipalname'] = principal
+ else:
+ failed[pkey] = _krb_err_msg % principal
+
+ return dn
+
+
+def _post_migrate_user(ldap, pkey, dn, entry_attrs, failed, config, ctx):
+ # add user to the default group
+ try:
+ ldap.add_entry_to_group(dn, ctx['def_group_dn'])
+ except errors.ExecutionError, e:
+ failed[pkey] = _grp_err_msg
+
+
+# GROUP MIGRATION CALLBACKS AND VARS
+
+def _pre_migrate_group(ldap, pkey, dn, entry_attrs, failed, config, ctx):
+ def convert_members(member_attr, overwrite=False):
+ """
+ Convert DNs in member attributes to work in IPA.
+ """
+ new_members = []
+ entry_attrs.setdefault(member_attr, [])
+ for m in entry_attrs[member_attr]:
+ col = m.find(',')
+ if col == -1:
+ continue
+ if m.startswith('uid'):
+ m = '%s,%s' % (m[0:col], api.env.container_user)
+ elif m.startswith('cn'):
+ m = '%s,%s' % (m[0:col], api.env.container_group)
+ m = ldap.normalize_dn(m)
+ new_members.append(m)
+ del entry_attrs[member_attr]
+ if overwrite:
+ entry_attrs['member'] = []
+ entry_attrs['member'] += new_members
+
+ entry_attrs['ipauniqueid'] = str(uuid.uuid1())
+ convert_members('member', overwrite=True)
+ convert_members('uniquemember')
+
+ return dn
+
+
+# DS MIGRATION PLUGIN
+
+def validate_ldapuri(ugettext, ldapuri):
+ m = re.match('^ldaps?://[-\w\.]+(:\d+)?$', ldapuri)
+ if not m:
+ err_msg = 'Invalid LDAP URI.'
+ raise errors.ValidationError(name='ldap_uri', error=err_msg)
+
+
+class migrate_ds(Command):
+ """
+ Migrate users and groups from DS to IPA.
+ """
+ migrate_objects = {
+ # OBJECT_NAME: (search_filter, pre_callback, post_callback)
+ #
+ # OBJECT_NAME - is the name of an LDAPObject subclass
+ # search_filter - is the filter to retrieve objects from DS
+ # pre_callback - is called for each object just after it was
+ # retrieved from DS and before being added to IPA
+ # post_callback - is called for each object after it was added to IPA
+ #
+ # {pre, post}_callback parameters:
+ # ldap - ldap2 instance connected to IPA
+ # pkey - primary key value of the object (uid for users, etc.)
+ # dn - dn of the object as it (will be/is) stored in IPA
+ # entry_attrs - attributes of the object
+ # failed - a list of so-far failed objects
+ # config - IPA config entry attributes
+ # ctx - object context, used to pass data between callbacks
+ #
+ # If pre_callback return value evaluates to False, migration
+ # of the current object is aborted.
+ 'user': (
+ '(&(objectClass=person)(uid=*))',
+ _pre_migrate_user, _post_migrate_user
+ ),
+ 'group': (
+ '(&(objectClass=groupOfUniqueNames)(cn=*))',
+ _pre_migrate_group, None
+ ),
+ }
+ migrate_order = ('user', 'group')
+
+ takes_args = (
+ Str('ldapuri', validate_ldapuri,
+ cli_name='ldap_uri',
+ doc='LDAP URI of DS server to migrate from',
+ ),
+ Password('bindpw',
+ cli_name='password',
+ doc='bind password',
+ ),
+ )
+
+ takes_options = (
+ Str('binddn?',
+ cli_name='bind_dn',
+ doc='bind DN',
+ default=u'cn=directory manager',
+ autofill=True,
+ ),
+ Str('usercontainer?',
+ cli_name='user_container',
+ doc='RDN of container for users in DS',
+ default=u'ou=people',
+ autofill=True,
+ ),
+ Str('groupcontainer?',
+ cli_name='group_container',
+ doc='RDN of container for groups in DS',
+ default=u'ou=groups',
+ autofill=True,
+ ),
+ )
+
+ has_output = (
+ output.Output('result',
+ type=dict,
+ doc='Lists of objects migrated; categorized by type.',
+ ),
+ output.Output('failed',
+ type=dict,
+ doc='Lists of objects that could not be migrated; ' \
+ 'categorized by type.',
+ ),
+ output.Output('enabled',
+ type=bool,
+ doc='False if migration mode was disabled.',
+ ),
+ )
+
+ exclude_doc = 'comma-separated list of %s to exclude from migration'
+ truncated_err_msg = 'search results for objects to be migrated ' \
+ 'have been truncated by the server; migration ' \
+ 'process might be uncomplete\n'
+ migration_disabled_msg = 'Migration mode is disabled. ' \
+ 'Use \'ipa config-mod\' to enable it.'
+ pwd_migration_msg = 'Passwords have been migrated in pre-hashed format. ' \
+ 'IPA is unable to generate Kerberos keys unless provided ' \
+ 'with clear text passwords. All migrated users need to ' \
+ 'login at https://your.domain/ipa/migration/ before they ' \
+ 'can use their Kerberos accounts.'
+
+ def get_options(self):
+ """
+ Call get_options of the baseclass and add "exclude" options
+ for each type of object being migrated.
+ """
+ for option in super(migrate_ds, self).get_options():
+ yield option
+ for ldap_obj_name in self.migrate_objects:
+ ldap_obj = self.api.Object[ldap_obj_name]
+ name = 'exclude_%ss' % to_cli(ldap_obj_name)
+ doc = self.exclude_doc % ldap_obj.object_name_plural
+ yield List(
+ '%s?' % name, cli_name=name, doc=doc, default=tuple(),
+ autofill=True
+ )
+
+ def normalize_options(self, options):
+ """
+ Convert all "exclude" option values to lower-case.
+
+ Also, empty List parameters are converted to None, but the migration
+ plugin doesn't like that - convert back to empty lists.
+ """
+ for p in self.params():
+ if isinstance(p, List):
+ if options[p.name]:
+ options[p.name] = tuple(
+ v.lower() for v in options[p.name]
+ )
+ else:
+ options[p.name] = tuple()
+
+ def migrate(self, ldap, config, ds_ldap, ds_base_dn, options):
+ """
+ Migrate objects from DS to LDAP.
+ """
+ migrated = {} # {'OBJ': ['PKEY1', 'PKEY2', ...], ...}
+ failed = {} # {'OBJ': {'PKEY1': 'Failed 'cos blabla', ...}, ...}
+ for ldap_obj_name in self.migrate_order:
+ ldap_obj = self.api.Object[ldap_obj_name]
+
+ search_filter = self.migrate_objects[ldap_obj_name][0]
+ search_base = '%s,%s' % (
+ options['%scontainer' % to_cli(ldap_obj_name)], ds_base_dn
+ )
+ exclude = options['exclude_%ss' % to_cli(ldap_obj_name)]
+ context = {}
+
+ migrated[ldap_obj_name] = []
+ failed[ldap_obj_name] = {}
+
+ # FIXME: with limits set, we get a strange 'Success' exception
+ (entries, truncated) = ds_ldap.find_entries(
+ search_filter, ['*'], search_base, ds_ldap.SCOPE_ONELEVEL#,
+ #time_limit=0, size_limit=0
+ )
+ if truncated:
+ self.log.error(
+ '%s: %s' % (
+ ldap_obj.object_name_plural, self.truncated_err_msg
+ )
+ )
+
+ for (dn, entry_attrs) in entries:
+ pkey = entry_attrs[ldap_obj.primary_key.name][0].lower()
+ if pkey in exclude:
+ continue
+
+ dn = ldap_obj.get_dn(pkey)
+ entry_attrs['objectclass'] = list(
+ set(
+ config.get(
+ ldap_obj.object_class_config, ldap_obj.object_class
+ ) + [o.lower() for o in entry_attrs['objectclass']]
+ )
+ )
+
+ callback = self.migrate_objects[ldap_obj_name][1]
+ if callable(callback):
+ dn = callback(
+ ldap, pkey, dn, entry_attrs, failed[ldap_obj_name],
+ config, context
+ )
+ if not dn:
+ continue
+
+ try:
+ ldap.add_entry(dn, entry_attrs)
+ except errors.ExecutionError, e:
+ failed[ldap_obj_name][pkey] = str(e)
+ else:
+ migrated[ldap_obj_name].append(pkey)
+
+ callback = self.migrate_objects[ldap_obj_name][2]
+ if callable(callback):
+ callback(
+ ldap, pkey, dn, entry_attrs, failed[ldap_obj_name],
+ config, context
+ )
+
+ return (migrated, failed)
+
+ def execute(self, ldapuri, bindpw, **options):
+ ldap = self.api.Backend.ldap2
+ self.normalize_options(options)
+
+ config = ldap.get_ipa_config()[1]
+
+ # check if migration mode is enabled
+ if config.get('ipamigrationenabled', ('FALSE', ))[0] == 'FALSE':
+ return dict(result={}, failed={}, enabled=False)
+
+ # connect to DS
+ ds_ldap = ldap2(shared_instance=False, ldap_uri=ldapuri, base_dn='')
+ ds_ldap.connect(bind_dn=options['binddn'], bind_pw=bindpw)
+
+ # retrieve DS base DN
+ (entries, truncated) = ds_ldap.find_entries(
+ '', ['namingcontexts'], '', ds_ldap.SCOPE_BASE
+ )
+ try:
+ ds_base_dn = entries[0][1]['namingcontexts'][0]
+ except (IndexError, KeyError), e:
+ raise StandardError(str(e))
+
+ # migrate!
+ (migrated, failed) = self.migrate(
+ ldap, config, ds_ldap, ds_base_dn, options
+ )
+
+ return dict(result=migrated, failed=failed, enabled=True)
+
+ def output_for_cli(self, textui, result, ldapuri, bindpw, **options):
+ textui.print_name(self.name)
+ if not result['enabled']:
+ textui.print_plain(self.migration_disabled_msg)
+ return 1
+ textui.print_plain('Migrated:')
+ textui.print_entry1(
+ result['result'], attr_order=self.migrate_order,
+ one_value_per_line=False
+ )
+ for ldap_obj_name in self.migrate_order:
+ textui.print_plain('Failed %s:' % ldap_obj_name)
+ textui.print_entry1(
+ result['failed'][ldap_obj_name], attr_order=self.migrate_order,
+ one_value_per_line=True,
+ )
+ textui.print_plain('-' * len(self.name))
+ textui.print_plain(self.pwd_migration_msg)
+
+api.register(migrate_ds)
+