Allow decoupling of user-private groups.

To do this we need to break the link manually on both sides, the user and
the group.

We also have to verify in advance that the user performing this is allowed
to do both. Otherwise the user could be decoupled but not the group
leaving it in a quasi broken state that only ldapmodify could fix.

ticket 75
This commit is contained in:
Rob Crittenden 2010-08-09 16:40:51 -04:00
parent 719592a209
commit 5b894d1fb7
4 changed files with 160 additions and 8 deletions

View File

@ -154,10 +154,10 @@ add:aci: '(targetattr = "givenName || sn || cn || displayName || title || initia
|| loginShell || gecos || homePhone || mobile || pager || facsimileTelephoneN || loginShell || gecos || homePhone || mobile || pager || facsimileTelephoneN
umber || telephoneNumber || street || roomNumber || l || st || postalCode || umber || telephoneNumber || street || roomNumber || l || st || postalCode ||
manager || secretary || description || carLicense || labeledURI || inetUserHT manager || secretary || description || carLicense || labeledURI || inetUserHT
TPURL || seeAlso || employeeType || businessCategory || ou")(target = "ldap:/ TPURL || seeAlso || employeeType || businessCategory || ou || mepManagedEntry
//uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "Modify User || objectclass")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")
s";allow (write) groupdn = "ldap:///cn=modifyusers,cn=taskgroups,cn=accounts, (version 3.0;acl "Modify Users";allow (write) groupdn =
$SUFFIX";)' "ldap:///cn=modifyusers,cn=taskgroups,cn=accounts,$SUFFIX";)'
# Add the taskgroups referenced by the ACIs for group administration # Add the taskgroups referenced by the ACIs for group administration
@ -204,10 +204,10 @@ add:aci: '(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version
askgroups,cn=accounts,$SUFFIX";)' askgroups,cn=accounts,$SUFFIX";)'
# we need objectclass and gidnumber in modify so a non-posix group can be # we need objectclass and gidnumber in modify so a non-posix group can be
# promoted # promoted
add:aci: '(targetattr = "cn || description || gidnumber || objectclass")(target add:aci: '(targetattr = "cn || description || gidnumber || objectclass ||
= "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "Modify Group mepManagedBy")(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")
s";allow (write) groupdn = "ldap:///cn=modifygroups,cn=taskgroups,cn=accounts, (version 3.0;acl "Modify Groups";allow (write) groupdn =
$SUFFIX";)' "ldap:///cn=modifygroups,cn=taskgroups,cn=accounts,$SUFFIX";)'
# Add the taskgroups referenced by the ACIs for host administration # Add the taskgroups referenced by the ACIs for host administration

View File

@ -1070,6 +1070,21 @@ class DNSNotARecordError(ExecutionError):
errno = 4019 errno = 4019
format = _('Host does not have corresponding DNS A record') format = _('Host does not have corresponding DNS A record')
class ManagedGroupError(ExecutionError):
"""
**4020** Raised when a managed group is deleted
For example:
>>> raise ManagedGroupError()
Traceback (most recent call last):
...
ManagedGroupError: Deleting a managed group is not allowed. It must be detached first.
"""
errno = 4020
format = _('Deleting a managed group is not allowed. It must be detached first.')
class BuiltinError(ExecutionError): class BuiltinError(ExecutionError):
""" """
**4100** Base class for builtin execution errors (*4100 - 4199*). **4100** Base class for builtin execution errors (*4100 - 4199*).

View File

@ -158,6 +158,9 @@ class group_del(LDAPDelete):
def_primary_group_dn = group_dn = self.obj.get_dn(def_primary_group) def_primary_group_dn = group_dn = self.obj.get_dn(def_primary_group)
if dn == def_primary_group_dn: if dn == def_primary_group_dn:
raise errors.DefaultGroup() raise errors.DefaultGroup()
(group_dn, group_attrs) = ldap.get_entry(dn)
if 'mepmanagedby' in group_attrs:
raise errors.ManagedGroupError()
return dn return dn
def post_callback(self, ldap, dn, *keys, **options): def post_callback(self, ldap, dn, *keys, **options):
@ -235,3 +238,59 @@ class group_remove_member(LDAPRemoveMember):
""" """
api.register(group_remove_member) api.register(group_remove_member)
class group_detach(LDAPRemoveMember):
"""
Detach a managed group from a user
"""
has_output = output.standard_value
msg_summary = _('Detached group "%(value)s" from user "%(value)s"')
def execute(self, *keys, **options):
"""
This requires updating both the user and the group. We first need to
verify that both the user and group can be updated, then we go
about our work. We don't want a situation where only the user or
group can be modified and we're left in a bad state.
"""
ldap = self.obj.backend
group_dn = self.obj.get_dn(*keys, **options)
user_dn = self.api.Object['user'].get_dn(*keys)
if (not ldap.can_write(user_dn, "objectclass") or
not ldap.can_write(user_dn, "mepManagedEntry")):
raise errors.ACIError(info=_('not allowed to modify user entries'))
if (not ldap.can_write(group_dn, "objectclass") or
not ldap.can_write(group_dn, "mepManagedBy")):
raise errors.ACIError(info=_('not allowed to modify group entries'))
(user_dn, user_attrs) = ldap.get_entry(user_dn)
objectclasses = user_attrs['objectclass']
try:
i = objectclasses.index('mepOriginEntry')
except ValueError:
raise NotFound(reason=_('Not a managed group'))
del objectclasses[i]
update_attrs = {'objectclass': objectclasses, 'mepManagedEntry': None}
ldap.update_entry(user_dn, update_attrs)
(group_dn, group_attrs) = ldap.get_entry(group_dn)
objectclasses = group_attrs['objectclass']
try:
i = objectclasses.index('mepManagedEntry')
except ValueError:
# this should never happen
raise NotFound(reason=_('Not a managed group'))
del objectclasses[i]
update_attrs = {'objectclass': objectclasses, 'mepManagedBy': None}
ldap.update_entry(group_dn, update_attrs)
return dict(
result=True,
value=keys[0],
)
api.register(group_detach)

View File

@ -27,6 +27,7 @@ from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
group1 = u'testgroup1' group1 = u'testgroup1'
group2 = u'testgroup2' group2 = u'testgroup2'
user1 = u'tuser1'
invalidgroup1=u'+tgroup1' invalidgroup1=u'+tgroup1'
invalidgroup2=u'tgroup1234567890123456789012345678901234567890' invalidgroup2=u'tgroup1234567890123456789012345678901234567890'
@ -36,6 +37,7 @@ class test_group(Declarative):
cleanup_commands = [ cleanup_commands = [
('group_del', [group1], {}), ('group_del', [group1], {}),
('group_del', [group2], {}), ('group_del', [group2], {}),
('user_del', [user1], {}),
] ]
tests = [ tests = [
@ -527,5 +529,81 @@ class test_group(Declarative):
expected=errors.ValidationError(name='cn', error='can be at most 33 characters'), expected=errors.ValidationError(name='cn', error='can be at most 33 characters'),
), ),
##### managed entry tests
dict(
desc='Create %r' % user1,
command=(
'user_add', [], dict(givenname=u'Test', sn=u'User1')
),
expected=dict(
value=user1,
summary=u'Added user "%s"' % user1,
result=dict(
gecos=[user1],
givenname=[u'Test'],
homedirectory=[u'/home/%s' % user1],
krbprincipalname=[u'%s@%s' % (user1, api.env.realm)],
loginshell=[u'/bin/sh'],
objectclass=objectclasses.user,
sn=[u'User1'],
uid=[user1],
uidnumber=[fuzzy_digits],
ipauniqueid=[fuzzy_uuid],
dn=u'uid=%s,cn=users,cn=accounts,%s' % (user1, api.env.basedn),
),
),
),
dict(
desc='Verify the managed group %r was created' % user1,
command=('group_show', [user1], {}),
expected=dict(
value=user1,
summary=None,
result=dict(
cn=[user1],
description=[u'User private group for %s' % user1],
gidnumber=[fuzzy_digits],
dn=u'cn=%s,cn=groups,cn=accounts,%s' % (user1, api.env.basedn),
),
),
),
dict(
desc='Try to delete a managed group %r' % user1,
command=('group_del', [user1], {}),
expected=errors.ManagedGroupError(),
),
dict(
desc='Detach managed group %r' % user1,
command=('group_detach', [user1], {}),
expected=dict(
result=True,
value=user1,
summary=u'Detached group "%s" from user "%s"' % (user1, user1),
),
),
dict(
desc='Now delete the unmanaged group %r' % user1,
command=('group_del', [user1], {}),
expected=dict(
result=True,
value=user1,
summary=u'Deleted group "%s"' % user1,
)
),
dict(
desc='Verify that %r is really gone' % user1,
command=('group_show', [user1], {}),
expected=errors.NotFound(reason='no such entry'),
),
] ]