diff --git a/ipa-admintools/Makefile b/ipa-admintools/Makefile index 4bed3b9a6..47822fc91 100644 --- a/ipa-admintools/Makefile +++ b/ipa-admintools/Makefile @@ -7,6 +7,10 @@ install: install -m 755 ipa-finduser $(SBINDIR) install -m 755 ipa-usermod $(SBINDIR) install -m 755 ipa-deluser $(SBINDIR) + install -m 755 ipa-addgroup $(SBINDIR) + install -m 755 ipa-delgroup $(SBINDIR) + install -m 755 ipa-findgroup $(SBINDIR) + install -m 755 ipa-groupmod $(SBINDIR) clean: rm -f *~ *.pyc diff --git a/ipa-admintools/ipa-finduser b/ipa-admintools/ipa-finduser index 167ac23d7..409d2e3de 100644 --- a/ipa-admintools/ipa-finduser +++ b/ipa-admintools/ipa-finduser @@ -50,7 +50,9 @@ def main(): client = ipaclient.IPAClient() users = client.find_users(args[1], sattrs=['dn','uid','cn','homeDirectory']) - if len(users) == 0: + counter = users[0] + users = users[1:] + if counter == 0: print "No entries found for", args[1] return 0 diff --git a/ipa-admintools/ipa-usermod b/ipa-admintools/ipa-usermod index 0c61f4097..317289a60 100644 --- a/ipa-admintools/ipa-usermod +++ b/ipa-admintools/ipa-usermod @@ -59,6 +59,9 @@ def main(): except ipa.ipaerror.IPAError, e: print "%s" % e.message return 1 + except kerberos.GSSError, e: + 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) @@ -79,6 +82,9 @@ def main(): except xmlrpclib.ProtocolError, e: print "Unable to connect to IPA server: %s" % (e.errmsg) return 1 + except ipa.ipaerror.IPAError, e: + print "%s" % (e.message) + return 1 return 0 diff --git a/ipa-python/ipautil.py b/ipa-python/ipautil.py index f6d62f7a3..2989b4211 100644 --- a/ipa-python/ipautil.py +++ b/ipa-python/ipautil.py @@ -30,6 +30,7 @@ import stat from string import lower import re import xmlrpclib +import datetime def realm_to_suffix(realm_name): s = realm_name.split(".") @@ -233,3 +234,100 @@ def unwrap_binary_data(data): else: return data +class GeneralizedTimeZone(datetime.tzinfo): + """This class is a basic timezone wrapper for the offset specified + in a Generalized Time. It is dst-ignorant.""" + def __init__(self,offsetstr="Z"): + super(GeneralizedTimeZone, self).__init__() + + self.name = offsetstr + self.houroffset = 0 + self.minoffset = 0 + + if offsetstr == "Z": + self.houroffset = 0 + self.minoffset = 0 + else: + if (len(offsetstr) >= 3) and re.match(r'[-+]\d\d', offsetstr): + self.houroffset = int(offsetstr[0:3]) + offsetstr = offsetstr[3:] + if (len(offsetstr) >= 2) and re.match(r'\d\d', offsetstr): + self.minoffset = int(offsetstr[0:2]) + offsetstr = offsetstr[2:] + if len(offsetstr) > 0: + raise ValueError() + if self.houroffset < 0: + self.minoffset *= -1 + + def utcoffset(self, dt): + return datetime.timedelta(hours=self.houroffset, minutes=self.minoffset) + + def dst(self, dt): + return datetime.timedelta(0) + + def tzname(self, dt): + return self.name + + +def parse_generalized_time(timestr): + """Parses are Generalized Time string (as specified in X.680), + returning a datetime object. Generalized Times are stored inside + the krbPasswordExpiration attribute in LDAP. + + This method doesn't attempt to be perfect wrt timezones. If python + can't be bothered to implement them, how can we...""" + + if len(timestr) < 8: + return None + try: + date = timestr[:8] + time = timestr[8:] + + year = int(date[:4]) + month = int(date[4:6]) + day = int(date[6:8]) + + hour = min = sec = msec = 0 + tzone = None + + if (len(time) >= 2) and re.match(r'\d', time[0]): + hour = int(time[:2]) + time = time[2:] + if len(time) >= 2 and (time[0] == "," or time[0] == "."): + hour_fraction = "." + time = time[1:] + while (len(time) > 0) and re.match(r'\d', time[0]): + hour_fraction += time[0] + time = time[1:] + total_secs = int(float(hour_fraction) * 3600) + min, sec = divmod(total_secs, 60) + + if (len(time) >= 2) and re.match(r'\d', time[0]): + min = int(time[:2]) + time = time[2:] + if len(time) >= 2 and (time[0] == "," or time[0] == "."): + min_fraction = "." + time = time[1:] + while (len(time) > 0) and re.match(r'\d', time[0]): + min_fraction += time[0] + time = time[1:] + sec = int(float(min_fraction) * 60) + + if (len(time) >= 2) and re.match(r'\d', time[0]): + sec = int(time[:2]) + time = time[2:] + if len(time) >= 2 and (time[0] == "," or time[0] == "."): + sec_fraction = "." + time = time[1:] + while (len(time) > 0) and re.match(r'\d', time[0]): + sec_fraction += time[0] + time = time[1:] + msec = int(float(sec_fraction) * 1000000) + + if (len(time) > 0): + tzone = GeneralizedTimeZone(time) + + return datetime.datetime(year, month, day, hour, min, sec, msec, tzone) + + except ValueError: + return None diff --git a/ipa-python/test/test_ipautil.py b/ipa-python/test/test_ipautil.py index 54ff1dc26..2755f71ea 100644 --- a/ipa-python/test/test_ipautil.py +++ b/ipa-python/test/test_ipautil.py @@ -21,6 +21,7 @@ import sys sys.path.insert(0, ".") import unittest +import datetime import ipautil @@ -207,6 +208,102 @@ class TestCIDict(unittest.TestCase): self.assert_(item in items) items.discard(item) +class TestTimeParser(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def testSimple(self): + timestr = "20070803" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(2007, time.year) + self.assertEqual(8, time.month) + self.assertEqual(3, time.day) + self.assertEqual(0, time.hour) + self.assertEqual(0, time.minute) + self.assertEqual(0, time.second) + + def testHourMinSec(self): + timestr = "20051213141205" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(2005, time.year) + self.assertEqual(12, time.month) + self.assertEqual(13, time.day) + self.assertEqual(14, time.hour) + self.assertEqual(12, time.minute) + self.assertEqual(5, time.second) + + def testFractions(self): + timestr = "2003092208.5" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(2003, time.year) + self.assertEqual(9, time.month) + self.assertEqual(22, time.day) + self.assertEqual(8, time.hour) + self.assertEqual(30, time.minute) + self.assertEqual(0, time.second) + + timestr = "199203301544,25" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(1992, time.year) + self.assertEqual(3, time.month) + self.assertEqual(30, time.day) + self.assertEqual(15, time.hour) + self.assertEqual(44, time.minute) + self.assertEqual(15, time.second) + + timestr = "20060401185912,8" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(2006, time.year) + self.assertEqual(4, time.month) + self.assertEqual(1, time.day) + self.assertEqual(18, time.hour) + self.assertEqual(59, time.minute) + self.assertEqual(12, time.second) + self.assertEqual(800000, time.microsecond) + + def testTimeZones(self): + timestr = "20051213141205Z" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(0, time.tzinfo.houroffset) + self.assertEqual(0, time.tzinfo.minoffset) + offset = time.tzinfo.utcoffset(None) + self.assertEqual(0, offset.seconds) + + timestr = "20051213141205+0500" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(5, time.tzinfo.houroffset) + self.assertEqual(0, time.tzinfo.minoffset) + offset = time.tzinfo.utcoffset(None) + self.assertEqual(5 * 60 * 60, offset.seconds) + + timestr = "20051213141205-0500" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(-5, time.tzinfo.houroffset) + self.assertEqual(0, time.tzinfo.minoffset) + # NOTE - the offset is always positive - it's minutes + # _east_ of UTC + offset = time.tzinfo.utcoffset(None) + self.assertEqual((24 - 5) * 60 * 60, offset.seconds) + + timestr = "20051213141205-0930" + + time = ipautil.parse_generalized_time(timestr) + self.assertEqual(-9, time.tzinfo.houroffset) + self.assertEqual(-30, time.tzinfo.minoffset) + offset = time.tzinfo.utcoffset(None) + self.assertEqual(((24 - 9) * 60 * 60) - (30 * 60), offset.seconds) + if __name__ == '__main__': unittest.main() diff --git a/ipa-server/freeipa-server.spec b/ipa-server/freeipa-server.spec index 05d84bc77..2d1c721cb 100755 --- a/ipa-server/freeipa-server.spec +++ b/ipa-server/freeipa-server.spec @@ -50,6 +50,7 @@ rm -rf %{buildroot} %attr(755,root,root) %{plugin_dir}/libipa_pwd_extop.so %attr(755,root,root) %{plugin_dir}/libipa-memberof-plugin.so +%attr(755,root,root) %{plugin_dir}/libipa-dna-plugin.so %changelog diff --git a/ipa-server/freeipa-server.spec.in b/ipa-server/freeipa-server.spec.in index 4071a409e..68cfee012 100644 --- a/ipa-server/freeipa-server.spec.in +++ b/ipa-server/freeipa-server.spec.in @@ -50,6 +50,7 @@ rm -rf %{buildroot} %attr(755,root,root) %{plugin_dir}/libipa_pwd_extop.so %attr(755,root,root) %{plugin_dir}/libipa-memberof-plugin.so +%attr(755,root,root) %{plugin_dir}/libipa-dna-plugin.so %changelog diff --git a/ipa-server/ipa-gui/ipagui/controllers.py b/ipa-server/ipa-gui/ipagui/controllers.py index 5fb4be06b..538e66b40 100644 --- a/ipa-server/ipa-gui/ipagui/controllers.py +++ b/ipa-server/ipa-gui/ipagui/controllers.py @@ -17,6 +17,7 @@ import ipa.ipaclient import ipa.user import xmlrpclib import forms.user +from helpers import userhelper from ipa import ipaerror ipa.config.init_config() @@ -47,6 +48,14 @@ class Root(controllers.RootController): def index(self): return dict() + @expose() + def topsearch(self, **kw): + if kw.get('searchtype') == "Users": + return self.userlist(uid=kw.get('searchvalue')) + else: + return self.index() + + ######## # User # @@ -107,7 +116,7 @@ class Root(controllers.RootController): def userupdate(self, **kw): """Updates an existing user""" restrict_post() - if kw.get('submit') == 'Cancel': + if kw.get('submit') == 'Cancel Edit': turbogears.flash("Edit user cancelled") raise turbogears.redirect('/usershow', uid=kw.get('uid')) @@ -188,7 +197,7 @@ class Root(controllers.RootController): def userindex(self): raise turbogears.redirect("/userlist") - @expose() + # @expose() def generate_password(self): password = "" generator = random.SystemRandom() @@ -203,6 +212,9 @@ class Root(controllers.RootController): if (len(givenname) == 0) or (len(sn) == 0): return "" + givenname = givenname.lower() + sn = sn.lower() + uid = givenname[0] + sn[:7] try: client.get_user_by_uid(uid) @@ -244,6 +256,9 @@ class Root(controllers.RootController): if (len(givenname) == 0) or (len(sn) == 0): return "" + givenname = givenname.lower() + sn = sn.lower() + # TODO - get from config domain = "freeipa.org" diff --git a/ipa-server/ipa-gui/ipagui/forms/user.py b/ipa-server/ipa-gui/ipagui/forms/user.py index b9b6f33d4..078e06ddd 100644 --- a/ipa-server/ipa-gui/ipagui/forms/user.py +++ b/ipa-server/ipa-gui/ipagui/forms/user.py @@ -3,29 +3,41 @@ from turbogears import validators, widgets class UserFields(): uid = widgets.TextField(name="uid", label="Login") - userpassword = widgets.TextField(name="userpassword", label="Password") + userpassword = widgets.PasswordField(name="userpassword", label="Password") + userpassword_confirm = widgets.PasswordField(name="userpassword_confirm", + label="Confirm Password") uidnumber = widgets.TextField(name="uidnumber", label="UID") gidnumber = widgets.TextField(name="gidnumber", label="GID") givenname = widgets.TextField(name="givenname", label="First name") sn = widgets.TextField(name="sn", label="Last name") mail = widgets.TextField(name="mail", label="E-mail address") telephonenumber = widgets.TextField(name="telephonenumber", label="Phone") - nsAccountLock = widgets.CheckBox(name="nsAccountLock", label="Account Deactivated") - - uid.validator = validators.PlainText(not_empty=True) - userpassword.validator = validators.String(not_empty=True) - givenname.validator = validators.String(not_empty=True) - sn.validator = validators.String(not_empty=True) - mail.validator = validators.Email(not_empty=True) - # validators.PhoneNumber may be a bit too picky, requiring an area code - telephonenumber.validator = validators.PlainText(not_empty=True) + # nsAccountLock = widgets.CheckBox(name="nsAccountLock", label="Account Deactivated") + nsAccountLock = widgets.SingleSelectField(name="nsAccountLock", + label="Account Status", + options = [("", "active"), ("true", "inactive")]) uid_hidden = widgets.HiddenField(name="uid") uidnumber_hidden = widgets.HiddenField(name="uidnumber") gidnumber_hidden = widgets.HiddenField(name="gidnumber") + krbPasswordExpiration_hidden = widgets.HiddenField(name="krbPasswordExpiration") user_orig = widgets.HiddenField(name="user_orig") +class UserNewValidator(validators.Schema): + uid = validators.PlainText(not_empty=True) + userpassword = validators.String(not_empty=False) + userpassword_confirm = validators.String(not_empty=False) + givenname = validators.String(not_empty=True) + sn = validators.String(not_empty=True) + mail = validators.Email(not_empty=True) + # validators.PhoneNumber may be a bit too picky, requiring an area code + # telephonenumber = validators.PlainText(not_empty=False) + + chained_validators = [ + validators.FieldsMatch('userpassword', 'userpassword_confirm') + ] + class UserNewForm(widgets.Form): params = ['user'] @@ -34,6 +46,8 @@ class UserNewForm(widgets.Form): UserFields.uidnumber, UserFields.gidnumber, UserFields.sn, UserFields.mail] + validator = UserNewValidator() + def __init__(self, *args, **kw): super(UserNewForm,self).__init__(*args, **kw) (self.template_c, self.template) = widgets.meta.load_kid_template("ipagui.templates.usernewform") @@ -46,6 +60,18 @@ class UserNewForm(widgets.Form): def has_foo(self): return False +class UserEditValidator(validators.Schema): + userpassword = validators.String(not_empty=False) + userpassword_confirm = validators.String(not_empty=False) + givenname = validators.String(not_empty=True) + sn = validators.String(not_empty=True) + mail = validators.Email(not_empty=True) + # validators.PhoneNumber may be a bit too picky, requiring an area code + # telephonenumber = validators.PlainText(not_empty=False) + + chained_validators = [ + validators.FieldsMatch('userpassword', 'userpassword_confirm') + ] class UserEditForm(widgets.Form): params = ['user'] @@ -53,8 +79,11 @@ class UserEditForm(widgets.Form): fields = [UserFields.givenname, UserFields.sn, UserFields.mail, UserFields.uid_hidden, UserFields.user_orig, UserFields.uidnumber_hidden, UserFields.gidnumber_hidden, + UserFields.krbPasswordExpiration_hidden, ] + validator = UserEditValidator() + def __init__(self, *args, **kw): super(UserEditForm,self).__init__(*args, **kw) (self.template_c, self.template) = widgets.meta.load_kid_template("ipagui.templates.usereditform") diff --git a/ipa-server/ipa-gui/ipagui/helpers/__init__.py b/ipa-server/ipa-gui/ipagui/helpers/__init__.py new file mode 100644 index 000000000..143f486c0 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/helpers/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/ipa-server/ipa-gui/ipagui/helpers/userhelper.py b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py new file mode 100644 index 000000000..e1ade3a2c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py @@ -0,0 +1,29 @@ +import sys +import datetime + +from ipa import ipautil + +def password_expires_in(datestr): + """Returns the number of days that password expires in. Returns a negative number + if the password is already expired.""" + if (datestr == None) or (datestr == ""): + return sys.maxint + + expdate = ipautil.parse_generalized_time(datestr) + if not expdate: + return sys.maxint + + delta = expdate - datetime.datetime.now() + return delta.days + +def password_is_expired(days): + return days < 0 + +def password_expires_soon(days): + return (not password_is_expired(days)) and (days < 7) + +def account_status_display(status): + if status == "true": + return "inactive" + else: + return "active" diff --git a/ipa-server/ipa-gui/ipagui/static/css/style.css b/ipa-server/ipa-gui/ipagui/static/css/style.css index 9ea86ae01..9654ebf15 100644 --- a/ipa-server/ipa-gui/ipagui/static/css/style.css +++ b/ipa-server/ipa-gui/ipagui/static/css/style.css @@ -17,6 +17,7 @@ body { background:#ccc; /* should be same as #sidebar */ margin:0 auto; width:100%; + clear:both; } @@ -24,9 +25,26 @@ body { background:#fff; } -#header h1 { - padding:5px; - margin:0; +#header #logo { + float:left; +} + +#header #headerinfo { + text-align:right; + padding-right:10px; +} + +#header #headerinfo #login { +} + +#header #headerinfo #topsearch { + padding-top: 15px; +} + +.searchtext { + background-color:#E5F1F4; + border:1px solid #8E8E8E; + color:#444444; } @@ -75,7 +93,11 @@ body { float:left; width:10%; padding: 5px; - font-size: small; + font-size: medium; +} + +#sidebar p { + line-height: 150%; } #sidebar h2 { @@ -140,6 +162,12 @@ body { font-weight: bolder; } +.warning_message { + font-size: 120%; + color: #ee0000; + font-weight: bolder; +} + .fielderror { color: red; font-weight: bold; diff --git a/ipa-server/ipa-gui/ipagui/templates/master.kid b/ipa-server/ipa-gui/ipagui/templates/master.kid index 2f39afc41..3be1f4c09 100644 --- a/ipa-server/ipa-gui/ipagui/templates/master.kid +++ b/ipa-server/ipa-gui/ipagui/templates/master.kid @@ -24,35 +24,66 @@ -