diff options
-rw-r--r-- | API.txt | 6 | ||||
-rw-r--r-- | VERSION | 2 | ||||
-rw-r--r-- | ipalib/plugins/user.py | 36 | ||||
-rw-r--r-- | ipapython/ipautil.py | 32 | ||||
-rw-r--r-- | tests/test_xmlrpc/test_user_plugin.py | 128 | ||||
-rw-r--r-- | tests/test_xmlrpc/xmlrpc_test.py | 10 |
6 files changed, 201 insertions, 13 deletions
@@ -2894,7 +2894,7 @@ output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None) output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) output: Output('value', <type 'unicode'>, None) command: user_add -args: 1,31,3 +args: 1,32,3 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', pattern_errmsg='may only include letters, numbers, _, -, . and $', primary_key=True, required=True) option: Str('givenname', attribute=True, cli_name='first', multivalue=False, required=True) option: Str('sn', attribute=True, cli_name='last', multivalue=False, required=True) @@ -2907,6 +2907,7 @@ option: Str('loginshell', attribute=True, cli_name='shell', default=u'/bin/sh', option: Str('krbprincipalname', attribute=True, autofill=True, cli_name='principal', multivalue=False, required=False) option: Str('mail', attribute=True, cli_name='email', multivalue=True, required=False) option: Password('userpassword', attribute=True, cli_name='password', exclude='webui', multivalue=False, required=False) +option: Flag('random', attribute=False, autofill=True, cli_name='random', default=False, multivalue=False, required=False) option: Int('uidnumber', attribute=True, autofill=True, cli_name='uid', default=999, minvalue=1, multivalue=False, required=True) option: Int('gidnumber', attribute=True, autofill=True, cli_name='gidnumber', multivalue=False, required=True) option: Str('street', attribute=True, cli_name='street', multivalue=False, required=False) @@ -3000,7 +3001,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list output: Output('count', <type 'int'>, None) output: Output('truncated', <type 'bool'>, None) command: user_mod -args: 1,32,3 +args: 1,33,3 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', pattern_errmsg='may only include letters, numbers, _, -, . and $', primary_key=True, query=True, required=True) option: Str('givenname', attribute=True, autofill=False, cli_name='first', multivalue=False, required=False) option: Str('sn', attribute=True, autofill=False, cli_name='last', multivalue=False, required=False) @@ -3012,6 +3013,7 @@ option: Str('gecos', attribute=True, autofill=False, cli_name='gecos', multivalu option: Str('loginshell', attribute=True, autofill=False, cli_name='shell', default=u'/bin/sh', multivalue=False, required=False) option: Str('mail', attribute=True, autofill=False, cli_name='email', multivalue=True, required=False) option: Password('userpassword', attribute=True, autofill=False, cli_name='password', exclude='webui', multivalue=False, required=False) +option: Flag('random', attribute=False, autofill=True, cli_name='random', default=False, multivalue=False, required=False) option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', default=999, minvalue=1, multivalue=False, required=False) option: Int('gidnumber', attribute=True, autofill=False, cli_name='gidnumber', multivalue=False, required=False) option: Str('street', attribute=True, autofill=False, cli_name='street', multivalue=False, required=False) @@ -79,4 +79,4 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=17 +IPA_API_VERSION_MINOR=18 diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py index a3c17dc4c..70a111dd3 100644 --- a/ipalib/plugins/user.py +++ b/ipalib/plugins/user.py @@ -25,6 +25,8 @@ from ipalib.request import context from time import gmtime, strftime import copy from ipalib import _, ngettext +from ipapython.ipautil import ipa_generate_password +import string __doc__ = _(""" Users @@ -74,6 +76,9 @@ user_output_params = ( ), ) +# characters to be used for generating random user passwords +user_pwdchars = string.digits + string.ascii_letters + '_,.@+-=' + def validate_nsaccountlock(entry_attrs): if 'nsaccountlock' in entry_attrs: nsaccountlock = entry_attrs['nsaccountlock'] @@ -238,6 +243,15 @@ class user(LDAPObject): # bomb out via the webUI. exclude='webui', ), + Flag('random?', + doc=_('Generate a random user password'), + flags=('no_search', 'virtual_attribute'), + default=False, + ), + Str('randompassword?', + label=_('Random password'), + flags=('no_create', 'no_update', 'no_search', 'virtual_attribute'), + ), Int('uidnumber', cli_name='uid', label=_('UID'), @@ -430,6 +444,11 @@ class user_add(LDAPCreate): raise errors.NotFound(reason=error_msg) entry_attrs['gidnumber'] = group_attrs['gidnumber'] + if 'userpassword' not in entry_attrs and options.get('random'): + entry_attrs['userpassword'] = ipa_generate_password(user_pwdchars) + # save the password so it can be displayed in post_callback + setattr(context, 'randompassword', entry_attrs['userpassword']) + if 'mail' in entry_attrs: entry_attrs['mail'] = self.obj._normalize_email(entry_attrs['mail'], config) @@ -465,6 +484,13 @@ class user_add(LDAPCreate): newentry = wait_for_value(ldap, dn, 'objectclass', 'mepOriginEntry') entry_from_entry(entry_attrs, newentry) + if options.get('random', False): + try: + entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) + except AttributeError: + # if both randompassword and userpassword options were used + pass + self.obj.get_password_attributes(ldap, dn, entry_attrs) return dn @@ -495,9 +521,19 @@ class user_mod(LDAPUpdate): if 'manager' in entry_attrs: entry_attrs['manager'] = self.obj._normalize_manager(entry_attrs['manager']) validate_nsaccountlock(entry_attrs) + if 'userpassword' not in entry_attrs and options.get('random'): + entry_attrs['userpassword'] = ipa_generate_password(user_pwdchars) + # save the password so it can be displayed in post_callback + setattr(context, 'randompassword', entry_attrs['userpassword']) return dn def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + if options.get('random', False): + try: + entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) + except AttributeError: + # if both randompassword and userpassword options were used + pass convert_nsaccountlock(entry_attrs) self.obj._convert_manager(entry_attrs, **options) self.obj.get_password_attributes(ldap, dn, entry_attrs) diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py index c06e7bbcf..44580be8e 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -550,21 +550,35 @@ def parse_generalized_time(timestr): except ValueError: return None -def ipa_generate_password(): +def ipa_generate_password(characters=None,pwd_len=None): + ''' Generates password. Password cannot start or end with a whitespace + character. It also cannot be formed by whitespace characters only. + Length of password as well as string of characters to be used by + generator could be optionaly specified by characters and pwd_len + parameters, otherwise default values will be used: characters string + will be formed by all printable non-whitespace characters and space, + pwd_len will be equal to value of GEN_PWD_LEN. + ''' + if not characters: + characters=string.digits + string.ascii_letters + string.punctuation + ' ' + else: + if characters.isspace(): + raise ValueError("password cannot be formed by whitespaces only") + if not pwd_len: + pwd_len = GEN_PWD_LEN + + upper_bound = len(characters) - 1 rndpwd = '' r = random.SystemRandom() - for x in range(GEN_PWD_LEN): - # do not generate space (chr(32)) as the first or last character - if x == 0 or x == (GEN_PWD_LEN-1): - rndchar = chr(r.randint(33,126)) - else: - rndchar = chr(r.randint(32,126)) + for x in range(pwd_len): + rndchar = characters[r.randint(0,upper_bound)] + if (x == 0) or (x == pwd_len-1): + while rndchar.isspace(): + rndchar = characters[r.randint(0,upper_bound)] rndpwd += rndchar - return rndpwd - def format_list(items, quote=None, page_width=80): '''Format a list of items formatting them so they wrap to fit the available width. The items will be sorted. diff --git a/tests/test_xmlrpc/test_user_plugin.py b/tests/test_xmlrpc/test_user_plugin.py index 8cd273960..0370ec74d 100644 --- a/tests/test_xmlrpc/test_user_plugin.py +++ b/tests/test_xmlrpc/test_user_plugin.py @@ -25,7 +25,7 @@ Test the `ipalib/plugins/user.py` module. from ipalib import api, errors from tests.test_xmlrpc import objectclasses -from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid +from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid, fuzzy_password, fuzzy_string, fuzzy_dergeneralizedtime from ipalib.dn import * user1=u'tuser1' @@ -722,6 +722,132 @@ class test_user(Declarative): ), ), + dict( + desc='Create %r with random password' % user1, + command=( + 'user_add', [user1], dict(givenname=u'Test', sn=u'User1', random=True) + ), + expected=dict( + value=user1, + summary=u'Added user "tuser1"', + result=dict( + gecos=[u'Test User1'], + givenname=[u'Test'], + homedirectory=[u'/home/tuser1'], + krbprincipalname=[u'tuser1@' + api.env.realm], + loginshell=[u'/bin/sh'], + objectclass=objectclasses.user, + sn=[u'User1'], + uid=[user1], + uidnumber=[fuzzy_digits], + gidnumber=[fuzzy_digits], + displayname=[u'Test User1'], + cn=[u'Test User1'], + initials=[u'TU'], + ipauniqueid=[fuzzy_uuid], + krbpwdpolicyreference=lambda x: [DN(i) for i in x] == \ + [DN(('cn','global_policy'),('cn',api.env.realm), + ('cn','kerberos'),api.env.basedn)], + mepmanagedentry=lambda x: [DN(i) for i in x] == \ + [DN(('cn',user1),('cn','groups'),('cn','accounts'), + api.env.basedn)], + memberof_group=[u'ipausers'], + has_keytab=True, + has_password=True, + randompassword=fuzzy_password, + krbextradata=[fuzzy_string], + krbpasswordexpiration=[fuzzy_dergeneralizedtime], + krblastpwdchange=[fuzzy_dergeneralizedtime], + dn=lambda x: DN(x) == \ + DN(('uid','tuser1'),('cn','users'),('cn','accounts'), + api.env.basedn), + ), + ), + ), + + dict( + desc='Delete %r' % user1, + command=('user_del', [user1], {}), + expected=dict( + result=dict(failed=u''), + summary=u'Deleted user "tuser1"', + value=user1, + ), + ), + + dict( + desc='Create %r' % user2, + command=( + 'user_add', [user2], dict(givenname=u'Test', sn=u'User2') + ), + expected=dict( + value=user2, + summary=u'Added user "tuser2"', + result=dict( + gecos=[u'Test User2'], + givenname=[u'Test'], + homedirectory=[u'/home/tuser2'], + krbprincipalname=[u'tuser2@' + api.env.realm], + loginshell=[u'/bin/sh'], + objectclass=objectclasses.user, + sn=[u'User2'], + uid=[user2], + uidnumber=[fuzzy_digits], + gidnumber=[fuzzy_digits], + displayname=[u'Test User2'], + cn=[u'Test User2'], + initials=[u'TU'], + ipauniqueid=[fuzzy_uuid], + krbpwdpolicyreference=lambda x: [DN(i) for i in x] == \ + [DN(('cn','global_policy'),('cn',api.env.realm), + ('cn','kerberos'),api.env.basedn)], + mepmanagedentry=lambda x: [DN(i) for i in x] == \ + [DN(('cn',user2),('cn','groups'),('cn','accounts'), + api.env.basedn)], + memberof_group=[u'ipausers'], + has_keytab=False, + has_password=False, + dn=lambda x: DN(x) == \ + DN(('uid','tuser2'),('cn','users'),('cn','accounts'), + api.env.basedn), + ), + ), + ), + + dict( + desc='Modify %r with random password' % user2, + command=( + 'user_mod', [user2], dict(random=True) + ), + expected=dict( + result=dict( + givenname=[u'Test'], + homedirectory=[u'/home/tuser2'], + loginshell=[u'/bin/sh'], + sn=[u'User2'], + uid=[user2], + uidnumber=[fuzzy_digits], + gidnumber=[fuzzy_digits], + memberof_group=[u'ipausers'], + nsaccountlock=False, + has_keytab=True, + has_password=True, + randompassword=fuzzy_password, + ), + summary=u'Modified user "tuser2"', + value=user2, + ), + ), + + dict( + desc='Delete %r' % user2, + command=('user_del', [user2], {}), + expected=dict( + result=dict(failed=u''), + summary=u'Deleted user "tuser2"', + value=user2, + ), + ), dict( desc='Create user %r with upper-case principal' % user1, diff --git a/tests/test_xmlrpc/xmlrpc_test.py b/tests/test_xmlrpc/xmlrpc_test.py index 4f29fb7ce..3535040e8 100644 --- a/tests/test_xmlrpc/xmlrpc_test.py +++ b/tests/test_xmlrpc/xmlrpc_test.py @@ -53,6 +53,16 @@ fuzzy_date = Fuzzy('^[a-zA-Z]{3} [a-zA-Z]{3} \d{2} \d{2}:\d{2}:\d{2} \d{4} UTC$' fuzzy_issuer = Fuzzy(type=basestring, test=lambda issuer: valid_issuer(issuer, api.env.realm)) +# Matches password - password consists of all printable characters without whitespaces +# The only exception is space, but space cannot be at the beggingin or end of the pwd +fuzzy_password = Fuzzy('^\S([\S ]*\S)*$') + +# Matches generalized time value. Time format is: %Y%m%d%H%M%SZ +fuzzy_dergeneralizedtime = Fuzzy('^[0-9]{14}Z$') + +# match any string +fuzzy_string = Fuzzy(type=basestring) + try: if not api.Backend.xmlclient.isconnected(): api.Backend.xmlclient.connect(fallback=False) |