service delegation: allow to add and remove host principals

Service delegation rules and targets deal with Kerberos principals.
As FreeIPA has separate service objects for hosts and Kerberos services,
it is not possible to specify host principal in the service delegation
rule or a target because the code assumes it always operates on Kerberos
service objects.

Simplify the code to add and remove members from delegation rules and
targets. New code looks up a name of the principal in cn=accounts,$BASEDN
as a krbPrincipalName attribute of an object with krbPrincipalAux object
class. This search path is optimized already for Kerberos KDC driver.

To support host principals, the specified principal name is checked to
have only one component (a host name). Service principals have more than
one component, typically service name and a host name, separated by '/'
sign. If the principal name has only one component, the name is
prepended with 'host/' to be able to find a host principal.

The logic described above allows to capture also aliases of both
Kerberos service and host principals. Additional check was added to
allow specifying single-component aliases ending with '$' sign. These
are typically used for Active Directory-related services like databases
or file services.

RN: service delegation rules and targets now allow to specify hosts as
RN: a rule or a target's member principal.

Fixes: https://pagure.io/freeipa/issue/8289
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Christian Heimes <cheimes@redhat.com>
This commit is contained in:
Alexander Bokovoy 2020-05-13 19:29:42 +03:00
parent 0fa31ef123
commit 1f82d281cc
2 changed files with 93 additions and 12 deletions

View File

@ -15,10 +15,10 @@ from .baseldap import (
LDAPDelete,
LDAPSearch,
LDAPRetrieve)
from .service import normalize_principal
from ipalib import _, ngettext
from ipalib import errors
from ipapython.dn import DN
from ipapython import kerberos
if six.PY3:
unicode = str
@ -46,6 +46,13 @@ A target consists of a list of principals that can be delegated.
In English, a rule says that this principal can delegate as this
list of principals, as defined by these targets.
In both a rule and a target Kerberos principals may be specified
by their name or an alias and the realm can be omitted. Additionally,
hosts can be specified by their names. If Kerberos principal specified
has a single component and does not end with '$' sign, it will be treated
as a host name. Kerberos principal names ending with '$' are typically
used as aliases for Active Directory-related services.
EXAMPLES:
Add a new constrained delegation rule:
@ -58,6 +65,10 @@ EXAMPLES:
ipa servicedelegationrule-add-member --principals=ftp/ipa.example.com \
ftp-delegation
Add a host principal of the host 'ipa.example.com' to the rule:
ipa servicedelegationrule-add-member --principals=ipa.example.com \
ftp-delegation
Add our target to the rule:
ipa servicedelegationrule-add-target \
--servicedelegationtargets=ftp-delegation-target ftp-delegation
@ -169,6 +180,21 @@ class servicedelegation(LDAPObject):
)
def normalize_principal_name(name, realm):
try:
princ = kerberos.Principal(name, realm=realm)
except ValueError as e:
raise errors.ValidationError(
name='principal',
reason=_("Malformed principal: %(error)s") % dict(error=str(e)))
if len(princ.components) == 1 and not princ.components[0].endswith('$'):
nprinc = 'host/' + unicode(princ)
else:
nprinc = unicode(princ)
return nprinc
class servicedelegation_add_member(LDAPAddMember):
__doc__ = _('Add target to a named service delegation.')
member_attrs = ['memberprincipal']
@ -212,22 +238,31 @@ class servicedelegation_add_member(LDAPAddMember):
failed[self.principal_failedattr] = {}
failed[self.principal_failedattr][self.principal_attr] = []
names = options.get(self.member_names[self.principal_attr], [])
ldap_obj = self.api.Object['service']
basedn = self.api.env.container_accounts + self.api.env.basedn
if names:
for name in names:
if not name:
continue
name = normalize_principal(name)
obj_dn = ldap_obj.get_dn(name)
princ = normalize_principal_name(name, self.api.env.realm)
try:
ldap.get_entry(obj_dn, ['krbprincipalname'])
e_attrs = ldap.find_entry_by_attr(
'krbprincipalname', princ, 'krbprincipalaux',
attrs_list=['krbprincipalname'],
base_dn=basedn)
except errors.NotFound as e:
failed[self.principal_failedattr][
self.principal_attr].append((name, unicode(e)))
continue
try:
if name not in entry_attrs.get(self.principal_attr, []):
members.append(name)
# normalize principal as set in krbPrincipalName attribute
mprinc = None
for p in e_attrs.get('krbprincipalname'):
p = unicode(p)
if p.lower() == princ.lower():
mprinc = p
if mprinc not in entry_attrs.get(self.principal_attr, []):
members.append(mprinc)
else:
raise errors.AlreadyGroupMember()
except errors.PublicError as e:
@ -314,10 +349,10 @@ class servicedelegation_remove_member(LDAPRemoveMember):
for name in names:
if not name:
continue
name = normalize_principal(name)
princ = normalize_principal_name(name, self.api.env.realm)
try:
if name in entry_attrs.get(self.principal_attr, []):
entry_attrs[self.principal_attr].remove(name)
if princ in entry_attrs.get(self.principal_attr, []):
entry_attrs[self.principal_attr].remove(princ)
else:
raise errors.NotGroupMember()
except errors.PublicError as e:

View File

@ -17,6 +17,8 @@ target1 = u'test1-targets'
target2 = u'test2-targets'
princ1 = u'HTTP/%s@%s' % (api.env.host, api.env.realm)
princ2 = u'ldap/%s@%s' % (api.env.host, api.env.realm)
princ3 = u'host/%s@%s' % (api.env.host, api.env.realm)
host3 = api.env.host
def get_servicedelegation_dn(cn):
@ -435,7 +437,7 @@ class test_servicedelegation(Declarative):
failed_memberprincipal=dict(
memberprincipal=[
[u'HTTP/notfound@%s' % api.env.realm,
u'no such entry']
u'no matching entry found']
],
),
),
@ -448,6 +450,50 @@ class test_servicedelegation(Declarative):
),
dict(
desc='Add host as a member %r to %r' % (host3, rule1),
command=(
'servicedelegationrule_add_member', [rule1],
dict(principal=princ3)
),
expected=dict(
completed=1,
failed=dict(
failed_memberprincipal=dict(
memberprincipal=tuple(),
),
),
result={
'dn': get_servicedelegation_dn(rule1),
'memberprincipal': (princ1, princ3),
'cn': [rule1],
},
),
),
dict(
desc='Remove a host member %r from %r' % (host3, rule1),
command=(
'servicedelegationrule_remove_member', [rule1],
dict(principal=host3)
),
expected=dict(
completed=1,
failed=dict(
failed_memberprincipal=dict(
memberprincipal=tuple(),
),
),
result={
'dn': get_servicedelegation_dn(rule1),
'memberprincipal': (princ1,),
'cn': [rule1],
},
),
),
dict(
desc='Remove a member %r from %r' % (princ1, rule1),
command=(
@ -556,7 +602,7 @@ class test_servicedelegation(Declarative):
failed_memberprincipal=dict(
memberprincipal=[
[u'HTTP/notfound@%s' % api.env.realm,
u'no such entry']
u'no matching entry found']
],
),
),