Only apply validation rules when adding and updating.

There may be cases, for whatever reason, that an otherwise illegal
entry gets created that doesn't match the criteria for a valid
user/host/group name. If this happens (i.e. migration) there is no way
to remove this using the IPA tools because we always applied the name
pattern. So you can't, for example, delete a user with an illegal name.

Primary keys are cloned with query=True in PKQuery which causes no
rules to be applied on mod/show/find. This reverts a change from commit
3a5e26a0 which applies class rules when query=True (for enforcing no
white space).

Replace rdnattr with rdn_is_primary_key. This was meant to tell us when
an RDN change was necessary to do a rename. There could be a disconnect
where the rdnattr wasn't the primary key and in that case we don't
need to do an RDN change, so use a boolean instead so that it is
clear that RDN == primary key.

Add a test to ensure that nowhitespace is actually enforced.

https://fedorahosted.org/freeipa/ticket/2115

Related: https://fedorahosted.org/freeipa/ticket/2089

Whitespace tickets:
https://fedorahosted.org/freeipa/ticket/1285
https://fedorahosted.org/freeipa/ticket/1286
https://fedorahosted.org/freeipa/ticket/1287
This commit is contained in:
Rob Crittenden 2012-02-29 13:31:20 -05:00
parent 87901ed709
commit 0099ccbea8
14 changed files with 145 additions and 31 deletions

24
API.txt
View File

@ -2036,12 +2036,12 @@ command: permission_add
args: 1,12,3
arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, required=True)
option: Str('permissions', attribute=True, cli_name='permissions', csv=True, multivalue=True, required=True)
option: Str('attrs', alwaysask=True, attribute=True, autofill=False, cli_name='attrs', csv=True, multivalue=True, query=True, required=False)
option: StrEnum('type', alwaysask=True, attribute=True, autofill=False, cli_name='type', multivalue=False, query=True, required=False, values=(u'user', u'group', u'host', u'service', u'hostgroup', u'netgroup', u'dnsrecord'))
option: Str('memberof', alwaysask=True, attribute=True, autofill=False, cli_name='memberof', multivalue=False, query=True, required=False)
option: Str('filter', alwaysask=True, attribute=True, autofill=False, cli_name='filter', multivalue=False, query=True, required=False)
option: Str('subtree', alwaysask=True, attribute=True, autofill=False, cli_name='subtree', multivalue=False, query=True, required=False)
option: Str('targetgroup', alwaysask=True, attribute=True, autofill=False, cli_name='targetgroup', multivalue=False, query=True, required=False)
option: Str('attrs', alwaysask=True, attribute=True, autofill=False, cli_name='attrs', csv=True, multivalue=True, query=False, required=False)
option: StrEnum('type', alwaysask=True, attribute=True, autofill=False, cli_name='type', multivalue=False, query=False, required=False, values=(u'user', u'group', u'host', u'service', u'hostgroup', u'netgroup', u'dnsrecord'))
option: Str('memberof', alwaysask=True, attribute=True, autofill=False, cli_name='memberof', multivalue=False, query=False, required=False)
option: Str('filter', alwaysask=True, attribute=True, autofill=False, cli_name='filter', multivalue=False, query=False, required=False)
option: Str('subtree', alwaysask=True, attribute=True, autofill=False, cli_name='subtree', multivalue=False, query=False, required=False)
option: Str('targetgroup', alwaysask=True, attribute=True, autofill=False, cli_name='targetgroup', multivalue=False, query=False, required=False)
option: Str('setattr*', cli_name='setattr', exclude='webui')
option: Str('addattr*', cli_name='addattr', exclude='webui')
option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
@ -2092,12 +2092,12 @@ command: permission_mod
args: 1,15,3
arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
option: Str('permissions', attribute=True, autofill=False, cli_name='permissions', csv=True, multivalue=True, required=False)
option: Str('attrs', alwaysask=True, attribute=True, autofill=False, cli_name='attrs', csv=True, multivalue=True, query=True, required=False)
option: StrEnum('type', alwaysask=True, attribute=True, autofill=False, cli_name='type', multivalue=False, query=True, required=False, values=(u'user', u'group', u'host', u'service', u'hostgroup', u'netgroup', u'dnsrecord'))
option: Str('memberof', alwaysask=True, attribute=True, autofill=False, cli_name='memberof', multivalue=False, query=True, required=False)
option: Str('filter', alwaysask=True, attribute=True, autofill=False, cli_name='filter', multivalue=False, query=True, required=False)
option: Str('subtree', alwaysask=True, attribute=True, autofill=False, cli_name='subtree', multivalue=False, query=True, required=False)
option: Str('targetgroup', alwaysask=True, attribute=True, autofill=False, cli_name='targetgroup', multivalue=False, query=True, required=False)
option: Str('attrs', alwaysask=True, attribute=True, autofill=False, cli_name='attrs', csv=True, multivalue=True, query=False, required=False)
option: StrEnum('type', alwaysask=True, attribute=True, autofill=False, cli_name='type', multivalue=False, query=False, required=False, values=(u'user', u'group', u'host', u'service', u'hostgroup', u'netgroup', u'dnsrecord'))
option: Str('memberof', alwaysask=True, attribute=True, autofill=False, cli_name='memberof', multivalue=False, query=False, required=False)
option: Str('filter', alwaysask=True, attribute=True, autofill=False, cli_name='filter', multivalue=False, query=False, required=False)
option: Str('subtree', alwaysask=True, attribute=True, autofill=False, cli_name='subtree', multivalue=False, query=False, required=False)
option: Str('targetgroup', alwaysask=True, attribute=True, autofill=False, cli_name='targetgroup', multivalue=False, query=False, required=False)
option: Str('setattr*', cli_name='setattr', exclude='webui')
option: Str('addattr*', cli_name='addattr', exclude='webui')
option: Str('delattr*', cli_name='delattr', exclude='webui')

View File

@ -144,7 +144,7 @@ class Create(Method):
continue
if 'ask_create' in option.flags:
yield option.clone(
attribute=attribute, query=True, required=False,
attribute=attribute, query=False, required=False,
autofill=False, alwaysask=True
)
else:
@ -161,6 +161,8 @@ class PKQuery(Method):
def get_args(self):
if self.obj.primary_key:
# Don't enforce rules on the primary key so we can reference
# any stored entry, legal or not
yield self.obj.primary_key.clone(attribute=True, query=True)
@ -189,7 +191,7 @@ class Update(PKQuery):
continue
if 'ask_update' in option.flags:
yield option.clone(
attribute=attribute, query=True, required=False,
attribute=attribute, query=False, required=False,
autofill=False, alwaysask=True
)
elif 'req_update' in option.flags:

View File

@ -508,7 +508,8 @@ class Param(ReadOnly):
self.class_rules = tuple(class_rules)
self.rules = rules
if self.query:
self.all_rules = self.class_rules
# by definition a query enforces no class or parameter rules
self.all_rules = ()
else:
self.all_rules = self.class_rules + self.rules
for rule in self.all_rules:

View File

@ -645,7 +645,7 @@ class automountkey(LDAPObject):
default_attributes = [
'automountkey', 'automountinformation', 'description'
]
rdnattr = 'description'
rdn_is_primary_key = True
rdn_separator = ' '
takes_params = (

View File

@ -429,7 +429,7 @@ class LDAPObject(Object):
rdn_attribute = ''
uuid_attribute = ''
attribute_members = {}
rdnattr = None
rdn_is_primary_key = False # Do we need RDN change to do a rename?
password_attributes = []
# Can bind as this entry (has userPassword or krbPrincipalKey)
bindable = False
@ -1178,7 +1178,7 @@ class LDAPUpdate(LDAPQuery, crud.Update):
has_output_params = global_output_params
def _get_rename_option(self):
rdnparam = getattr(self.obj.params, self.obj.rdnattr)
rdnparam = getattr(self.obj.params, self.obj.primary_key.name)
return rdnparam.clone_rename('rename',
cli_name='rename', required=False, label=_('Rename'),
doc=_('Rename the %(ldap_obj_name)s object') % dict(
@ -1189,7 +1189,7 @@ class LDAPUpdate(LDAPQuery, crud.Update):
def get_options(self):
for option in super(LDAPUpdate, self).get_options():
yield option
if self.obj.rdnattr:
if self.obj.rdn_is_primary_key:
yield self._get_rename_option()
def execute(self, *keys, **options):
@ -1229,18 +1229,19 @@ class LDAPUpdate(LDAPQuery, crud.Update):
rdnupdate = False
try:
if self.obj.rdnattr and 'rename' in options:
if self.obj.rdn_is_primary_key and 'rename' in options:
if not options['rename']:
raise errors.ValidationError(name='rename', error=u'can\'t be empty')
entry_attrs[self.obj.rdnattr] = options['rename']
entry_attrs[self.obj.primary_key.name] = options['rename']
if self.obj.rdnattr and self.obj.rdnattr in entry_attrs:
if self.obj.rdn_is_primary_key and self.obj.primary_key.name in entry_attrs:
# RDN change
ldap.update_entry_rdn(dn, unicode('%s=%s' % (self.obj.rdnattr,
entry_attrs[self.obj.rdnattr])))
rdnkeys = keys[:-1] + (entry_attrs[self.obj.rdnattr], )
ldap.update_entry_rdn(dn,
unicode('%s=%s' % (self.obj.primary_key.name,
entry_attrs[self.obj.primary_key.name])))
rdnkeys = keys[:-1] + (entry_attrs[self.obj.primary_key.name], )
dn = self.obj.get_dn(*rdnkeys)
del entry_attrs[self.obj.rdnattr]
del entry_attrs[self.obj.primary_key.name]
options['rdnupdate'] = True
rdnupdate = True

View File

@ -95,7 +95,7 @@ class group(LDAPObject):
'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule',
'sudorule'],
}
rdnattr = 'cn'
rdn_is_primary_key = True
label = _('User Groups')
label_singular = _('User Group')

View File

@ -144,7 +144,7 @@ class permission(LDAPObject):
attribute_members = {
'member': ['privilege'],
}
rdnattr='cn'
rdn_is_primary_key = True
label = _('Permissions')
label_singular = _('Permission')

View File

@ -60,7 +60,7 @@ class privilege(LDAPObject):
reverse_members = {
'member': ['permission'],
}
rdnattr='cn'
rdn_is_primary_key = True
label = _('Privileges')
label_singular = _('Privilege')

View File

@ -76,7 +76,7 @@ class role(LDAPObject):
reverse_members = {
'member': ['privilege'],
}
rdnattr='cn'
rdn_is_primary_key = True
label = _('Roles')
label_singular = _('Role')

View File

@ -168,7 +168,7 @@ class user(LDAPObject):
'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'],
'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'],
}
rdnattr = 'uid'
rdn_is_primary_key = True
bindable = True
password_attributes = [('userpassword', 'has_password'),
('krbprincipalkey', 'has_keytab')]

View File

@ -602,6 +602,28 @@ class test_group(Declarative):
expected=errors.ValidationError(name='cn', error='may only include letters, numbers, _, -, . and $'),
),
# The assumption on these next 4 tests is that if we don't get a
# validation error then the request was processed normally.
dict(
desc='Test that validation is disabled on mods',
command=('group_mod', [invalidgroup1], {}),
expected=errors.NotFound(reason='no such entry'),
),
dict(
desc='Test that validation is disabled on deletes',
command=('group_del', [invalidgroup1], {}),
expected=errors.NotFound(reason='no such entry'),
),
dict(
desc='Test that validation is disabled on show',
command=('group_show', [invalidgroup1], {}),
expected=errors.NotFound(reason='no such entry'),
),
##### managed entry tests
dict(

View File

@ -47,6 +47,7 @@ dn3 = DN(('fqdn',fqdn3),('cn','computers'),('cn','accounts'),
fqdn4 = u'testhost2.lab.%s' % api.env.domain
dn4 = DN(('fqdn',fqdn4),('cn','computers'),('cn','accounts'),
api.env.basedn)
invalidfqdn1 = u'foo_bar.lab.%s' % api.env.domain
# We can use the same cert we generated for the service tests
fd = open('tests/test_xmlrpc/service.crt', 'r')
@ -670,4 +671,45 @@ class test_host(Declarative):
expected=errors.ValidationError(name='fqdn', error='An IPA master host cannot be deleted'),
),
dict(
desc='Test that validation is enabled on adds',
command=('host_add', [invalidfqdn1], {}),
expected=errors.ValidationError(name='fqdn', error='may only include letters, numbers, and -'),
),
# The assumption on these next 4 tests is that if we don't get a
# validation error then the request was processed normally.
dict(
desc='Test that validation is disabled on mods',
command=('host_mod', [invalidfqdn1], {}),
expected=errors.NotFound(reason='no such entry'),
),
dict(
desc='Test that validation is disabled on deletes',
command=('host_del', [invalidfqdn1], {}),
expected=errors.NotFound(reason='no such entry'),
),
dict(
desc='Test that validation is disabled on show',
command=('host_show', [invalidfqdn1], {}),
expected=errors.NotFound(reason='no such entry'),
),
dict(
desc='Test that validation is disabled on find',
command=('host_find', [invalidfqdn1], {}),
expected=dict(
count=0,
truncated=False,
summary=u'0 hosts matched',
result=[],
),
),
]

View File

@ -33,6 +33,7 @@ role1 = u'test-role-1'
role1_dn = DN(('cn',role1),api.env.container_rolegroup,
api.env.basedn)
renamedrole1 = u'test-role'
invalidrole1 = u' whitespace '
role2 = u'test-role-2'
role2_dn = DN(('cn',role2),api.env.container_rolegroup,
@ -100,6 +101,15 @@ class test_role(Declarative):
),
dict(
desc='Create invalid %r' % invalidrole1,
command=('role_add', [invalidrole1],
dict(description=u'role desc 1')
),
expected=errors.ValidationError(name='cn', error='Leading and trailing spaces are not allowed'),
),
dict(
desc='Create %r' % role1,
command=('role_add', [role1],

View File

@ -643,6 +643,42 @@ class test_user(Declarative):
expected=errors.ValidationError(name='uid', error='can be at most 33 characters'),
),
# The assumption on these next 4 tests is that if we don't get a
# validation error then the request was processed normally.
dict(
desc='Test that validation is disabled on deletes',
command=('user_del', [invaliduser1], {}),
expected=errors.NotFound(reason='no such entry'),
),
dict(
desc='Test that validation is disabled on show',
command=('user_show', [invaliduser1], {}),
expected=errors.NotFound(reason='no such entry'),
),
dict(
desc='Test that validation is disabled on find',
command=('user_find', [invaliduser1], {}),
expected=dict(
count=0,
truncated=False,
summary=u'0 users matched',
result=[],
),
),
dict(
desc='Try to rename to invalid username %r' % user1,
command=('user_mod', [user1], dict(rename=invaliduser1)),
expected=errors.ValidationError(name='uid', error='may only include letters, numbers, _, -, . and $'),
),
dict(
desc='Create %r' % group1,
command=(