# HG changeset patch # User rcritten@redhat.com # Date 1190384676 14400 # Node ID 7e2ea9789d1f32b965eb292eb6a6290c48bba626 # Parent ccc013039e2da7bbb80532c48cd73e51a9b0b7ba Give ipa-adduser, ipa-addgroup and ipa-usermod an interactive mode Add ipa-passwd tool Add simple field validation package This patch adds a package requirement, python-krbV. This is needed to determine the current user based on their kerberos ticket. diff -r ccc013039e2d -r 7e2ea9789d1f ipa-admintools/Makefile --- a/ipa-admintools/Makefile Thu Sep 20 14:53:23 2007 -0700 +++ b/ipa-admintools/Makefile Fri Sep 21 10:24:36 2007 -0400 @@ -11,6 +11,7 @@ install: install -m 755 ipa-delgroup $(SBINDIR) install -m 755 ipa-findgroup $(SBINDIR) install -m 755 ipa-groupmod $(SBINDIR) + install -m 755 ipa-passwd $(SBINDIR) clean: rm -f *~ *.pyc diff -r ccc013039e2d -r 7e2ea9789d1f ipa-admintools/ipa-addgroup --- a/ipa-admintools/ipa-addgroup Thu Sep 20 14:53:23 2007 -0700 +++ b/ipa-admintools/ipa-addgroup Fri Sep 21 10:24:36 2007 -0400 @@ -23,6 +23,7 @@ import ipa import ipa import ipa.group import ipa.ipaclient as ipaclient +import ipa.ipavalidate as ipavalidate import ipa.config import ipa.ipaerror @@ -49,22 +50,51 @@ def parse_options(): return options, args def main(): + cn = "" + desc = "" + group=ipa.group.Group() options, args = parse_options() - if len(args) != 2: - usage() + cont = False - group.setValue('cn', args[1]) - if options.desc: - group.setValue('description', options.desc) + if (len(args) != 2): + while (cont != True): + cn = raw_input("Group name: ") + if (ipavalidate.plain(cn, notEmpty=True)): + print "Field is required and must be letters or '." + else: + cont = True + else: + cn = args[1] + if (ipavalidate.plain(cn, notEmpty=True)): + print "Group name is required and must be letters or '." + return 1 + + cont = False + if not options.desc: + while (cont != True): + desc = raw_input("Description: ") + if (ipavalidate.plain(desc, notEmpty=True)): + print "Field is required and must be letters or '." + else: + cont = True + else: + desc = options.desc + if (ipavalidate.plain(desc, notEmpty=True)): + print "First name is required and must be letters or '." + return 1 + if options.gid: group.setValue('gidnumber', options.gid) + + group.setValue('cn', cn) + group.setValue('description', desc) try: client = ipaclient.IPAClient() client.add_group(group) - print args[1] + " successfully added" + print cn + " successfully added" except xmlrpclib.Fault, f: print f.faultString return 1 diff -r ccc013039e2d -r 7e2ea9789d1f ipa-admintools/ipa-adduser --- a/ipa-admintools/ipa-adduser Thu Sep 20 14:53:23 2007 -0700 +++ b/ipa-admintools/ipa-adduser Fri Sep 21 10:24:36 2007 -0400 @@ -23,11 +23,13 @@ import ipa import ipa import ipa.user import ipa.ipaclient as ipaclient +import ipa.ipavalidate as ipavalidate import ipa.config import xmlrpclib import kerberos import ldap +import getpass def usage(): print "ipa-adduser [-c|--gecos STRING] [-d|--directory STRING] [-f|--firstname STRING] [-l|--lastname STRING] user" @@ -47,37 +49,168 @@ def parse_options(): help="Set user's password") parser.add_option("-s", "--shell", dest="shell", help="Set user's login shell to shell") + parser.add_option("-G", "--groups", dest="groups", + help="Add account to one or more groups (comma-separated)") + parser.add_option("-M", "--mailAddress", dest="mail", + help="Set uesr's e-mail address") parser.add_option("--usage", action="store_true", help="Program usage") args = ipa.config.init_config(sys.argv) options, args = parser.parse_args(args) - if not options.gn or not options.sn: - usage() - return options, args def main(): + # The following fields are required + givenname = "" + lastname = "" + username = "" + password = "" + mail = "" + gecos = "" + directory = "" + shell = "" + groups = "" + + match = False + cont = False + + all_interactive = False + user=ipa.user.User() options, args = parse_options() if len(args) != 2: - usage() - - user.setValue('givenname', options.gn) - user.setValue('sn', options.sn) - user.setValue('uid', args[1]) - if options.gecos: - user.setValue('gecos', options.gecos) - if options.directory: - user.setValue('homedirectory', options.directory) - if options.shell: - user.setValue('loginshell', options.shell) - else: - user.setValue('loginshell', "/bin/bash") - - username = args[1] + all_interactive = True + + if not options.gn: + while (cont != True): + givenname = raw_input("First name: ") + if (ipavalidate.plain(givenname, notEmpty=True)): + print "Field is required and must be letters or '" + else: + cont = True + else: + givenname = options.gn + if (ipavalidate.plain(givenname, notEmpty=True)): + print "First name is required and must be letters or '" + return 1 + + cont = False + if not options.sn: + while (cont != True): + lastname = raw_input(" Last name: ") + if (ipavalidate.plain(lastname, notEmpty=True)): + print "Field is required and must be letters or '" + else: + cont = True + else: + lastname = options.sn + if (ipavalidate.plain(lastname, notEmpty=True)): + print "Last name is required and must be letters or '" + return 1 + + cont = False + if (len(args) != 2): + while (cont != True): + username = raw_input("Login name: ") + if (ipavalidate.plain(username, notEmpty=True)): + print "Field is required and must be letters or '" + else: + cont = True + else: + username = args[1] + if (ipavalidate.plain(username, notEmpty=True)): + print "Username is required and must be letters or '" + return 1 + + if not options.password: + while (match != True): + password = getpass.getpass(" Password: ") + confirm = getpass.getpass(" Password (again): ") + if (password != confirm): + print "Passwords do not match" + match = False + else: + match = True + if (len(password) < 1): + print "Password cannot be empty" + match = False + else: + password = options.sn + + cont = False + if not options.mail: + while (cont != True): + mail = raw_input("E-mail addr: ") + if (ipavalidate.email(mail)): + print "Field is required and must include a user and domain name" + else: + cont = True + else: + mail = options.mail + if (ipavalidate.email(mail)): + print "E-mail is required and must include a user and domain name" + return 1 + + # Ask the questions we don't normally force. We don't require answers + # for these. + if all_interactive is True: + cont = False + if not options.gecos: + while (cont != True): + gecos = raw_input("gecos []: ") + if (ipavalidate.plain(gecos, notEmpty=False)): + print "Must be letters, numbers, spaces or '" + else: + cont = True + cont = False + if not options.directory: + while (cont != True): + directory = raw_input("home directory []: ") + if (ipavalidate.path(gecos, notEmpty=False)): + print "Must be letters, numbers, spaces or '" + else: + cont = True + cont = False + if not options.shell: + while (cont != True): + shell = raw_input("shell [/bin/sh]: ") + + if len(shell) < 1: + shell = None + cont = True + cont = False + if not options.groups: + while (cont != True): + g = raw_input("Add to group [blank to exit]: ") + + if len(g) < 1: + cont = True + else: + if (ipavalidate.path(g, notEmpty=False)): + print "Must be letters, numbers, spaces or '" + else: + groups = groups + "," + g + else: + gecos = options.gecos + directory = options.directory + shell = options.shell + groups = options.groups + + user.setValue('givenname', givenname) + user.setValue('sn', lastname) + user.setValue('uid', username) + user.setValue('mail', mail) + if gecos: + user.setValue('gecos', gecos) + if directory: + user.setValue('homedirectory', directory) + if shell: + user.setValue('loginshell', shell) + else: + user.setValue('loginshell', "/bin/sh") try: client = ipaclient.IPAClient() @@ -95,12 +228,25 @@ def main(): print "%s" % (e.message) return 1 - if options.password is not None: + # Set the User's password + if password is not None: try: - client.modifyPassword(username, None, options.password) + client.modifyPassword(username, None, password) except ipa.ipaerror.IPAError, e: + print "User added but setting the password failed." print "%s" % (e.message) return 1 + + # Add to any groups + if groups: + add_groups = groups.split(',') + for g in add_groups: + if g: + try: + client.add_user_to_group(username, g) + print "%s added to group %s" % (username, g) + except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_NOT_FOUND): + print "group %s doesn't exist, skipping" % g print username + " successfully added" return 0 diff -r ccc013039e2d -r 7e2ea9789d1f ipa-admintools/ipa-usermod --- a/ipa-admintools/ipa-usermod Thu Sep 20 14:53:23 2007 -0700 +++ b/ipa-admintools/ipa-usermod Fri Sep 21 10:24:36 2007 -0400 @@ -21,14 +21,17 @@ import sys import sys from optparse import OptionParser import ipa +import ipa.user import ipa.ipaclient as ipaclient +import ipa.ipavalidate as ipavalidate import ipa.config import xmlrpclib import kerberos +import ldap def usage(): - print "ipa-usermod [-c|--gecos STRING] [-d|--directory STRING] user" + print "ipa-usermod [-c|--gecos STRING] [-d|--directory STRING] [-f|--firstname STRING] [-l|--lastname STRING] user" sys.exit(1) def parse_options(): @@ -37,8 +40,14 @@ def parse_options(): help="Set the GECOS field") parser.add_option("-d", "--directory", dest="directory", help="Set the User's home directory") + parser.add_option("-f", "--firstname", dest="gn", + help="User's first name") + parser.add_option("-l", "--lastname", dest="sn", + help="User's last name") parser.add_option("-s", "--shell", dest="shell", help="Set user's login shell to shell") + parser.add_option("-M", "--mailAddress", dest="mail", + help="Set uesr's e-mail address") parser.add_option("--usage", action="store_true", help="Program usage") @@ -48,14 +57,32 @@ def parse_options(): return options, args def main(): + # The following fields are required + givenname = "" + lastname = "" + username = "" + mail = "" + gecos = "" + directory = "" + groups = "" + shell = "" + + match = False + cont = False + options, args = parse_options() if len(args) != 2: usage() + username = args[1] + client = ipaclient.IPAClient() try: - user = client.get_user_by_uid(args[1]) + user = client.get_user_by_uid(username) + except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_NOT_FOUND): + print "User %s not found" % username + return 1 except ipa.ipaerror.IPAError, e: print "%s" % e.message return 1 @@ -63,16 +90,107 @@ def main(): print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0]) return 1 - if options.gecos: - user.setValue('gecos', options.gecos) - if options.directory: - user.setValue('homedirectory', options.directory) - if options.shell: - user.setValue('loginshell', options.shell) + # If any options are set we use just those. Otherwise ask for all of them. + if options.gn or options.sn or options.directory or options.gecos or options.mail: + givenname = options.gn + lastname = options.sn + gecos = options.gecos + directory = options.directory + mail = options.mail + else: + if not options.gn: + while (cont != True): + givenname = raw_input("First name: [%s] " % user.getValue('givenname')) + if (ipavalidate.plain(givenname, notEmpty=False)): + print "Must be letters or '" + else: + cont = True + if len(givenname) < 1: + shell = None + cont = True + else: + givenname = options.gn + if (ipavalidate.plain(givenname, notEmpty=True)): + print "First name must be letters or '" + return 1 + + cont = False + if not options.sn: + while (cont != True): + lastname = raw_input(" Last name: [%s] " % user.getValue('sn')) + if (ipavalidate.plain(lastname, notEmpty=False)): + print "Must be letters or '" + else: + cont = True + if len(lastname) < 1: + shell = None + cont = True + else: + lastname = options.sn + if (ipavalidate.plain(lastname, notEmpty=True)): + print "Last name must be letters or '" + return 1 + + cont = False + if not options.mail: + while (cont != True): + mail = raw_input("E-mail addr: [%s]" % user.getValue('mail')) + if (ipavalidate.email(mail, notEmpty=False)): + print "Must include a user and domain name" + else: + cont = True + else: + mail = options.mail + if (ipavalidate.email(mail)): + print "E-mail must include a user and domain name" + return 1 + + # Ask the questions we don't normally force. We don't require answers + # for these. + cont = False + if not options.gecos: + while (cont != True): + gecos = raw_input("gecos: [%s] " % user.getValue('gecos')) + if (ipavalidate.plain(gecos, notEmpty=False)): + print "Must be letters, numbers, spaces or '" + else: + cont = True + cont = False + if not options.directory: + while (cont != True): + directory = raw_input("home directory: [%s] " % user.getValue('homeDirectory')) + if (ipavalidate.path(gecos, notEmpty=False)): + print "Must be letters, numbers, spaces or '" + else: + cont = True + cont = False + if not options.shell: + while (cont != True): + shell = raw_input("shell: [%s] " % user.getValue('loginshell')) + + if len(shell) < 1: + shell = None + cont = True + cont = False + + if givenname: + user.setValue('givenname', givenname) + if lastname: + user.setValue('sn', lastname) + if mail: + user.setValue('mail', mail) + user.setValue('cn', "%s %s" % (user.getValue('givenname'), + user.getValue('sn'))) + + if gecos: + user.setValue('gecos', gecos) + if directory: + user.setValue('homedirectory', directory) + if shell: + user.setValue('loginshell', shell) try: client.update_user(user) - print args[1] + " successfully modified" except xmlrpclib.Fault, f: print f.faultString return 1 @@ -86,6 +204,7 @@ def main(): print "%s" % (e.message) return 1 + print username + " successfully updated" return 0 main() diff -r ccc013039e2d -r 7e2ea9789d1f ipa-python/freeipa-python.spec --- a/ipa-python/freeipa-python.spec Thu Sep 20 14:53:23 2007 -0700 +++ b/ipa-python/freeipa-python.spec Fri Sep 21 10:24:36 2007 -0400 @@ -10,7 +10,7 @@ BuildRoot: %{_tmppath}/%{name}-%{ve BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildArch: noarch -Requires: python PyKerberos +Requires: python PyKerberos python-krbV %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} diff -r ccc013039e2d -r 7e2ea9789d1f ipa-server/ipa-install/README --- a/ipa-server/ipa-install/README Thu Sep 20 14:53:23 2007 -0700 +++ b/ipa-server/ipa-install/README Fri Sep 21 10:24:36 2007 -0400 @@ -19,6 +19,8 @@ gcc gcc python-ldap TurboGears +PyKerberos +python-krbV Installation example: diff -r 7e2ea9789d1f ipa-admintools/ipa-passwd --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ipa-admintools/ipa-passwd Fri Sep 21 10:23:48 2007 -0400 @@ -0,0 +1,99 @@ +#! /usr/bin/python -E +# Authors: Rob Crittenden +# +# Copyright (C) 2007 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 Tempal Place, Suite 330, Boston, MA 02111-1307 USA +# + +import sys +from optparse import OptionParser +import ipa +import ipa.ipaclient as ipaclient +import ipa.config + +import xmlrpclib +import kerberos +import krbV +import ldap +import getpass + +def usage(): + print "ipa-passwd [user]" + sys.exit(1) + +def parse_options(): + parser = OptionParser() + parser.add_option("--usage", action="store_true", + help="Program usage") + + args = ipa.config.init_config(sys.argv) + options, args = parser.parse_args(args) + + return options, args + +def get_principal(): + try: + ctx = krbV.default_context() + ccache = ctx.default_ccache() + cprinc = ccache.principal() + except krbV.Krb5Error, e: + print "Unable to get kerberos principal: %s" % e[1] + return None + + return cprinc.name + +def main(): + match = False + + options, args = parse_options() + + if len(args) == 2: + username = args[1] + else: + username = get_principal() + if username is None: + return 1 + + u = username.split('@') + if len(u) > 1: + username = u[0] + + print "Changing password for %s" % username + + while (match != True): + # No syntax checking of the password is required because that is done + # on the server side + password = getpass.getpass(" New Password: ") + confirm = getpass.getpass(" New Password (again): ") + if (password != confirm): + print "Passwords do not match" + match = False + else: + match = True + if (len(password) < 1): + print "Password cannot be empty" + match = False + + try: + client = ipaclient.IPAClient() + client.modifyPassword(username, None, password) + except ipa.ipaerror.IPAError, e: + print "%s" % (e.message) + return 1 + + return 0 + +main() diff -r 7e2ea9789d1f ipa-python/ipavalidate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ipa-python/ipavalidate.py Fri Sep 21 10:23:48 2007 -0400 @@ -0,0 +1,99 @@ +#! /usr/bin/python -E +# Authors: Rob Crittenden +# +# Copyright (C) 2007 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 or later +# +# 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 +# + +import re + +def email(mail, notEmpty=True): + """Do some basic validation of an e-mail address. + Return 0 if ok + Return 1 if not + + If notEmpty is True the this will return an error if the field + is "" or None. + """ + usernameRE = re.compile(r"^[^ \t\n\r@<>()]+$", re.I) + domainRE = re.compile(r"^[a-z0-9][a-z0-9\.\-_]*\.[a-z]+$", re.I) + + if not mail or mail is None: + if notEmpty is True: + return 1 + else: + return 0 + + mail = mail.strip() + s = mail.split('@', 1) + try: + username, domain=s + except ValueError: + return 1 + if not usernameRE.search(username): + return 1 + if not domainRE.search(domain): + return 1 + + return 0 + +def plain(text, notEmpty=False): + """Do some basic validation of a plain text field + Return 0 if ok + Return 1 if not + + If notEmpty is True the this will return an error if the field + is "" or None. + """ + textRE = re.compile(r"^[a-zA-Z_\-0-9\'\ ]*$") + + if not text and notEmpty is True: + return 1 + + if text is None: + if notEmpty is True: + return 1 + else: + return 0 + + if not textRE.search(text): + return 1 + + return 0 + +def path(text, notEmpty=False): + """Do some basic validation of a path + Return 0 if ok + Return 1 if not + + If notEmpty is True the this will return an error if the field + is "" or None. + """ + textRE = re.compile(r"^[a-zA-Z_\-0-9\\ \.\/\\:]*$") + + if not text and notEmpty is True: + return 1 + + if text is None: + if notEmpty is True: + return 1 + else: + return 0 + + if not textRE.search(text): + return 1 + + return 0 diff -r 7e2ea9789d1f ipa-python/test/test_ipavalidate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ipa-python/test/test_ipavalidate.py Fri Sep 21 10:23:48 2007 -0400 @@ -0,0 +1,70 @@ +#! /usr/bin/python -E +# +# Copyright (C) 2007 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 or later +# +# 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 +# + +import sys +sys.path.insert(0, ".") + +import unittest + +import ipavalidate + +class TestValidate(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_validemail(self): + self.assertEqual(0, ipavalidate.email("test@freeipa.org")) + self.assertEqual(0, ipavalidate.email("", notEmpty=False)) + + def test_invalidemail(self): + self.assertEqual(1, ipavalidate.email("test")) + self.assertEqual(1, ipavalidate.email("test@freeipa")) + self.assertEqual(1, ipavalidate.email("test@.com")) + self.assertEqual(1, ipavalidate.email("")) + self.assertEqual(1, ipavalidate.email(None)) + + def test_validplain(self): + self.assertEqual(0, ipavalidate.plain("Joe User")) + self.assertEqual(0, ipavalidate.plain("Joe O'Malley")) + self.assertEqual(0, ipavalidate.plain("", notEmpty=False)) + self.assertEqual(0, ipavalidate.plain(None, notEmpty=False)) + + def test_invalidplain(self): + self.assertEqual(1, ipavalidate.plain("Joe (User)")) + self.assertEqual(1, ipavalidate.plain("", notEmpty=True)) + self.assertEqual(1, ipavalidate.plain(None, notEmpty=True)) + + def test_validpath(self): + self.assertEqual(0, ipavalidate.path("/")) + self.assertEqual(0, ipavalidate.path("/home/user")) + self.assertEqual(0, ipavalidate.path("../home/user")) + self.assertEqual(0, ipavalidate.path("", notEmpty=False)) + self.assertEqual(0, ipavalidate.path(None, notEmpty=False)) + + def test_invalidpath(self): + self.assertEqual(1, ipavalidate.path("(foo)")) + self.assertEqual(1, ipavalidate.path("", notEmpty=True)) + self.assertEqual(1, ipavalidate.path(None, notEmpty=True)) + +if __name__ == '__main__': + unittest.main() +