group: allow services as members of groups

Allow services to be members of the groups, like users and other groups
can already be.

This is required for use cases where such services aren't associated
with a particular host (and thus, the host object cannot be used to
retrieve the keytabs) but represent purely client Kerberos principals to
use in a dynamically generated environment such as Kubernetes.

Fixes: https://pagure.io/freeipa/issue/7513
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
This commit is contained in:
Alexander Bokovoy 2018-04-26 11:32:19 +03:00 committed by Rob Crittenden
parent 9e8fb94e87
commit e642865717
9 changed files with 89 additions and 17 deletions

10
API.txt
View File

@ -1944,13 +1944,14 @@ output: Entry('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: group_add_member/1
args: 1,7,3
args: 1,8,3
arg: Str('cn', cli_name='group_name')
option: Flag('all', autofill=True, cli_name='all', default=False)
option: Str('group*', alwaysask=True, cli_name='groups')
option: Str('ipaexternalmember*', cli_name='external')
option: Flag('no_members', autofill=True, default=False)
option: Flag('raw', autofill=True, cli_name='raw', default=False)
option: Str('service*', alwaysask=True, cli_name='services')
option: Str('user*', alwaysask=True, cli_name='users')
option: Str('version?')
output: Output('completed', type=[<type 'int'>])
@ -1972,7 +1973,7 @@ output: Output('result', type=[<type 'bool'>])
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: group_find/1
args: 1,28,4
args: 1,30,4
arg: Str('criteria?')
option: Flag('all', autofill=True, cli_name='all', default=False)
option: Str('cn?', autofill=False, cli_name='group_name')
@ -1987,6 +1988,7 @@ option: Str('in_role*', cli_name='in_roles')
option: Str('in_sudorule*', cli_name='in_sudorules')
option: Str('no_group*', cli_name='no_groups')
option: Flag('no_members', autofill=True, default=True)
option: Principal('no_service*', cli_name='no_services')
option: Str('no_user*', cli_name='no_users')
option: Flag('nonposix', autofill=True, cli_name='nonposix', default=False)
option: Str('not_in_group*', cli_name='not_in_groups')
@ -1998,6 +2000,7 @@ option: Flag('pkey_only?', autofill=True, default=False)
option: Flag('posix', autofill=True, cli_name='posix', default=False)
option: Flag('private', autofill=True, cli_name='private', default=False)
option: Flag('raw', autofill=True, cli_name='raw', default=False)
option: Principal('service*', cli_name='services')
option: Int('sizelimit?', autofill=False)
option: Int('timelimit?', autofill=False)
option: Str('user*', cli_name='users')
@ -2026,13 +2029,14 @@ output: Entry('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: group_remove_member/1
args: 1,7,3
args: 1,8,3
arg: Str('cn', cli_name='group_name')
option: Flag('all', autofill=True, cli_name='all', default=False)
option: Str('group*', alwaysask=True, cli_name='groups')
option: Str('ipaexternalmember*', cli_name='external')
option: Flag('no_members', autofill=True, default=False)
option: Flag('raw', autofill=True, cli_name='raw', default=False)
option: Str('service*', alwaysask=True, cli_name='services')
option: Str('user*', alwaysask=True, cli_name='users')
option: Str('version?')
output: Output('completed', type=[<type 'int'>])

View File

@ -128,6 +128,10 @@ return {
$type: 'association',
name: 'member_group'
},
{
$type: 'association',
name: 'member_service'
},
{
$type: 'attribute',
name: 'member_external',

View File

@ -62,10 +62,10 @@ if api.env.in_server and api.env.context in ['lite', 'server']:
__doc__ = _("""
Groups of users
Manage groups of users. By default, new groups are POSIX groups. You
can add the --nonposix option to the group-add command to mark a new group
as non-POSIX. You can use the --posix argument with the group-mod command
to convert a non-POSIX group into a POSIX group. POSIX groups cannot be
Manage groups of users, groups, or services. By default, new groups are POSIX
groups. You can add the --nonposix option to the group-add command to mark a
new group as non-POSIX. You can use the --posix argument with the group-mod
command to convert a non-POSIX group into a POSIX group. POSIX groups cannot be
converted to non-POSIX groups.
Every group must have a description.
@ -75,6 +75,10 @@ supported but can have an impact on your file permissions. It is not necessary
to supply a GID when creating a group. IPA will generate one automatically
if it is not provided.
Groups members can be users, other groups, and Kerberos services. In POSIX
environments only users will be visible as group members, but nested groups and
groups of services can be used for IPA management purposes.
EXAMPLES:
Add a new group:
@ -101,6 +105,9 @@ EXAMPLES:
Add multiple users to the "localadmins" group:
ipa group-add-member --users=test1 --users=test2 localadmins
To add Kerberos services to the "printer admins" group:
ipa group-add-member --services=CUPS/some.host printeradmins
Remove a user from the "localadmins" group:
ipa group-remove-member --users=test2 localadmins
@ -171,9 +178,9 @@ class group(LDAPObject):
]
uuid_attribute = 'ipauniqueid'
attribute_members = {
'member': ['user', 'group'],
'member': ['user', 'group', 'service'],
'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'],
'memberindirect': ['user', 'group'],
'memberindirect': ['user', 'group', 'service'],
'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule',
'sudorule'],
}

View File

@ -625,7 +625,7 @@ class service_add(LDAPCreate):
except errors.NotFound:
raise errors.NotFound(reason=_(
"The host '%s' does not exist to add a service to.") %
hostname)
hostname)
self.obj.validate_ipakrbauthzdata(entry_attrs)

View File

@ -320,6 +320,7 @@ class test_netgroup(Declarative):
member=dict(
group=tuple(),
user=tuple(),
service=tuple(),
),
),
result={

View File

@ -240,6 +240,7 @@ class test_selinuxusermap(Declarative):
member=dict(
group=tuple(),
user=tuple(),
service=tuple(),
),
),
result={

View File

@ -890,6 +890,7 @@ class test_service_allowed_to(Declarative):
cleanup_commands = [
('user_del', [user1], {}),
('user_del', [user2], {}),
('service_del', [d_service], {}),
('group_del', [group1], {}),
('group_del', [group2], {}),
('host_del', [fqdn1], {}),
@ -938,6 +939,40 @@ class test_service_allowed_to(Declarative):
),
),
),
# Create a service disconnected from any host
dict(
desc='Try to create service %r without any host' % d_service,
command=('service_add', [d_service],
dict(force=True, skip_host_check=True)),
expected=dict(
value=d_service,
summary=u'Added service "%s"' % d_service,
result=dict(
dn=d_servicedn,
krbprincipalname=[d_service],
krbcanonicalname=[d_service],
objectclass=objectclasses.service,
ipauniqueid=[fuzzy_uuid],
),
),
),
dict(
desc='Add service %r to a group: %r' % (d_service, group1),
command=('group_add_member', [group1],
dict(service=[d_service_no_realm])),
expected=dict(
completed=1,
failed=dict(member=dict(group=[],
service=[],
user=[])),
result=dict(
cn=[group1],
gidnumber=[fuzzy_digits],
dn=group1_dn,
member_service=[d_service],
),
),
),
dict(
desc='Create group: %r' % group2,
command=(

View File

@ -12,9 +12,10 @@ from ipatests.util import assert_deepequal, get_group_dn
class GroupTracker(Tracker):
""" Class for host plugin like tests """
retrieve_keys = {u'dn', u'cn', u'gidnumber', u'member_user',
u'member_group', u'description',
u'member_group', u'member_service', u'description',
u'memberof_group', u'memberofindirect_group',
u'memberindirect_group', u'memberindirect_user'}
u'memberindirect_group', u'memberindirect_user',
u'memberindirect_service'}
retrieve_all_keys = retrieve_keys | {u'ipauniqueid', u'objectclass'}
@ -112,7 +113,7 @@ class GroupTracker(Tracker):
)
def add_member(self, options):
""" Add a member (group OR user) and performs check """
""" Add a member (group OR user OR service) and performs check """
if u'user' in options:
try:
self.attrs[u'member_user'] =\
@ -125,6 +126,12 @@ class GroupTracker(Tracker):
self.attrs[u'member_group'] + [options[u'group']]
except KeyError:
self.attrs[u'member_group'] = [options[u'group']]
elif u'service' in options:
try:
self.attrs[u'member_service'] =\
self.attrs[u'member_service'] + [options[u'service']]
except KeyError:
self.attrs[u'member_service'] = [options[u'service']]
command = self.make_add_member_command(options)
result = command()
@ -136,6 +143,8 @@ class GroupTracker(Tracker):
self.attrs[u'member_user'].remove(options[u'user'])
elif u'group' in options:
self.attrs[u'member_group'].remove(options[u'group'])
elif u'service' in options:
self.attrs[u'member_service'].remove(options[u'service'])
try:
if not self.attrs[u'member_user']:
@ -147,6 +156,11 @@ class GroupTracker(Tracker):
del self.attrs[u'member_group']
except KeyError:
pass
try:
if not self.attrs[u'member_service']:
del self.attrs[u'member_service']
except KeyError:
pass
command = self.make_remove_member_command(options)
result = command()
@ -207,7 +221,7 @@ class GroupTracker(Tracker):
""" Checks 'group_add_member' command result """
assert_deepequal(dict(
completed=1,
failed={u'member': {u'group': (), u'user': ()}},
failed={u'member': {u'group': (), u'user': (), u'service': ()}},
result=self.filter_attrs(self.add_member_keys)
), result)
@ -216,7 +230,7 @@ class GroupTracker(Tracker):
when expected result is failure of the operation"""
expected = dict(
completed=0,
failed={u'member': {u'group': (), u'user': ()}},
failed={u'member': {u'group': (), u'user': (), u'service': ()}},
result=self.filter_attrs(self.add_member_keys)
)
if not options:
@ -230,6 +244,9 @@ class GroupTracker(Tracker):
elif u'group' in options:
expected[u'failed'][u'member'][u'group'] = [(
options[u'group'], u'no such entry')]
elif u'service' in options:
expected[u'failed'][u'member'][u'service'] = [(
options[u'service'], u'no such entry')]
assert_deepequal(expected, result)
@ -238,7 +255,7 @@ class GroupTracker(Tracker):
when expected result is failure of the operation"""
expected = dict(
completed=0,
failed={u'member': {u'group': (), u'user': ()}},
failed={u'member': {u'group': (), u'user': (), u'service': ()}},
result=self.filter_attrs(self.add_member_keys)
)
if u'user' in options:
@ -247,6 +264,9 @@ class GroupTracker(Tracker):
elif u'group' in options:
expected[u'failed'][u'member'][u'group'] = [(
options[u'group'], u'This entry is not a member')]
elif u'service' in options:
expected[u'failed'][u'member'][u'service'] = [(
options[u'service'], u'This entry is not a member')]
assert_deepequal(expected, result)

View File

@ -514,7 +514,7 @@ class UserTracker(CertmapdataMixin, KerberosAliasMixin, Tracker):
assert_deepequal(dict(
completed=1,
failed=dict(
member=dict(group=tuple(), user=tuple())
member=dict(group=tuple(), user=tuple(), service=tuple())
),
result={
'dn': get_group_dn(admin_group),