mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-25 00:20:04 -06:00
8e4408e678
The rename operation on *_mod commands was only allowed when the primary key of an entry was also its RDN. With these changes, it should be possible to rename the rest of the entries as well. An attribute to the base LDAPObject was added to whitelist the objects we want to allow to be renamed. It replaced an old attribute rdn_is_primary_key which was used for the very same purpose but the name was confusing because it was not set correctly for certain objects. https://pagure.io/freeipa/issue/2466 https://pagure.io/freeipa/issue/6784 Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Jan Cholasta <jcholast@redhat.com> Reviewed-By: Martin Basti <mbasti@redhat.com>
528 lines
17 KiB
Python
528 lines
17 KiB
Python
#
|
|
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
|
#
|
|
|
|
import six
|
|
|
|
from ipalib import api
|
|
from ipalib import Str
|
|
from ipalib.plugable import Registry
|
|
from .baseldap import (
|
|
LDAPObject,
|
|
LDAPAddMember,
|
|
LDAPRemoveMember,
|
|
LDAPCreate,
|
|
LDAPDelete,
|
|
LDAPSearch,
|
|
LDAPRetrieve)
|
|
from .service import normalize_principal
|
|
from ipalib import _, ngettext
|
|
from ipalib import errors
|
|
from ipapython.dn import DN
|
|
|
|
if six.PY3:
|
|
unicode = str
|
|
|
|
__doc__ = _("""
|
|
Service Constrained Delegation
|
|
|
|
Manage rules to allow constrained delegation of credentials so
|
|
that a service can impersonate a user when communicating with another
|
|
service without requiring the user to actually forward their TGT.
|
|
This makes for a much better method of delegating credentials as it
|
|
prevents exposure of the short term secret of the user.
|
|
|
|
The naming convention is to append the word "target" or "targets" to
|
|
a matching rule name. This is not mandatory but helps conceptually
|
|
to associate rules and targets.
|
|
|
|
A rule consists of two things:
|
|
- A list of targets the rule applies to
|
|
- A list of memberPrincipals that are allowed to delegate for
|
|
those targets
|
|
|
|
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.
|
|
|
|
EXAMPLES:
|
|
|
|
Add a new constrained delegation rule:
|
|
ipa servicedelegationrule-add ftp-delegation
|
|
|
|
Add a new constrained delegation target:
|
|
ipa servicedelegationtarget-add ftp-delegation-target
|
|
|
|
Add a principal to the rule:
|
|
ipa servicedelegationrule-add-member --principals=ftp/ipa.example.com \
|
|
ftp-delegation
|
|
|
|
Add our target to the rule:
|
|
ipa servicedelegationrule-add-target \
|
|
--servicedelegationtargets=ftp-delegation-target ftp-delegation
|
|
|
|
Add a principal to the target:
|
|
ipa servicedelegationtarget-add-member --principals=ldap/ipa.example.com \
|
|
ftp-delegation-target
|
|
|
|
Display information about a named delegation rule and target:
|
|
ipa servicedelegationrule_show ftp-delegation
|
|
ipa servicedelegationtarget_show ftp-delegation-target
|
|
|
|
Remove a constrained delegation:
|
|
ipa servicedelegationrule-del ftp-delegation-target
|
|
ipa servicedelegationtarget-del ftp-delegation
|
|
|
|
In this example the ftp service can get a TGT for the ldap service on
|
|
the bound user's behalf.
|
|
|
|
It is strongly discouraged to modify the delegations that ship with
|
|
IPA, ipa-http-delegation and its targets ipa-cifs-delegation-targets and
|
|
ipa-ldap-delegation-targets. Incorrect changes can remove the ability
|
|
to delegate, causing the framework to stop functioning.
|
|
""")
|
|
|
|
register = Registry()
|
|
|
|
PROTECTED_CONSTRAINT_RULES = (
|
|
u'ipa-http-delegation',
|
|
)
|
|
|
|
PROTECTED_CONSTRAINT_TARGETS = (
|
|
u'ipa-cifs-delegation-targets',
|
|
u'ipa-ldap-delegation-targets',
|
|
|
|
)
|
|
|
|
|
|
class servicedelegation(LDAPObject):
|
|
"""
|
|
Service Constrained Delegation base object.
|
|
|
|
This jams a couple of concepts into a single plugin because the
|
|
data is all stored in one place. There is a "rule" which has the
|
|
objectclass ipakrb5delegationacl. This is the entry that controls
|
|
the delegation. Other entries that lack this objectclass are
|
|
targets and define what services can be impersonated.
|
|
"""
|
|
container_dn = api.env.container_s4u2proxy
|
|
object_class = ['groupofprincipals', 'top']
|
|
|
|
managed_permissions = {
|
|
'System: Read Service Delegations': {
|
|
'ipapermbindruletype': 'permission',
|
|
'ipapermright': {'read', 'search', 'compare'},
|
|
'ipapermtargetfilter': {'(objectclass=groupofprincipals)'},
|
|
'ipapermdefaultattr': {
|
|
'cn', 'objectclass', 'memberprincipal',
|
|
'ipaallowedtarget',
|
|
},
|
|
'default_privileges': {'Service Administrators'},
|
|
},
|
|
'System: Add Service Delegations': {
|
|
'ipapermright': {'add'},
|
|
'ipapermtargetfilter': {'(objectclass=groupofprincipals)'},
|
|
'default_privileges': {'Service Administrators'},
|
|
},
|
|
'System: Remove Service Delegations': {
|
|
'ipapermright': {'delete'},
|
|
'ipapermtargetfilter': {'(objectclass=groupofprincipals)'},
|
|
'default_privileges': {'Service Administrators'},
|
|
},
|
|
'System: Modify Service Delegation Membership': {
|
|
'ipapermright': {'write'},
|
|
'ipapermtargetfilter': {'(objectclass=groupofprincipals)'},
|
|
'ipapermdefaultattr': {'memberprincipal', 'ipaallowedtarget'},
|
|
'default_privileges': {'Service Administrators'},
|
|
},
|
|
}
|
|
|
|
allow_rename = True
|
|
|
|
takes_params = (
|
|
Str(
|
|
'cn',
|
|
pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]*[a-zA-Z0-9_.-]?$',
|
|
pattern_errmsg='may only include letters, numbers, _, -, ., '
|
|
'and a space inside',
|
|
maxlength=255,
|
|
cli_name='delegation_name',
|
|
label=_('Delegation name'),
|
|
primary_key=True,
|
|
),
|
|
Str(
|
|
'ipaallowedtarget_servicedelegationtarget',
|
|
label=_('Allowed Target'),
|
|
flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
|
|
),
|
|
Str(
|
|
'ipaallowedtoimpersonate',
|
|
label=_('Allowed to Impersonate'),
|
|
flags={'no_create', 'no_update', 'no_search'},
|
|
),
|
|
Str(
|
|
'memberprincipal',
|
|
label=_('Member principals'),
|
|
flags={'no_create', 'no_update', 'no_search'},
|
|
),
|
|
)
|
|
|
|
|
|
class servicedelegation_add_member(LDAPAddMember):
|
|
__doc__ = _('Add target to a named service delegation.')
|
|
member_attrs = ['memberprincipal']
|
|
member_attributes = []
|
|
member_names = {}
|
|
principal_attr = 'memberprincipal'
|
|
principal_failedattr = 'failed_memberprincipal'
|
|
|
|
def get_options(self):
|
|
for option in super(servicedelegation_add_member, self).get_options():
|
|
yield option
|
|
for attr in self.member_attrs:
|
|
name = self.member_names[attr]
|
|
doc = self.member_param_doc % name
|
|
yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
|
|
label=_('member %s') % name, alwaysask=True)
|
|
|
|
def get_member_dns(self, **options):
|
|
"""
|
|
There are no member_dns to return. memberPrincipal needs
|
|
special handling since it is just a principal, not a
|
|
full dn.
|
|
"""
|
|
return dict(), dict()
|
|
|
|
def post_callback(self, ldap, completed, failed, dn, entry_attrs,
|
|
*keys, **options):
|
|
"""
|
|
Add memberPrincipal values. This is done afterward because it isn't
|
|
a DN and the LDAPAddMember method explicitly only handles DNs.
|
|
|
|
A separate fake attribute name is used for failed members. This is
|
|
a reverse of the way this is typically handled in the *Member
|
|
routines, where a successful addition will be represented as
|
|
member/memberof_<attribute>. In this case, because memberPrincipal
|
|
isn't a DN, I'm doing the reverse, and creating a fake failed
|
|
attribute instead.
|
|
"""
|
|
ldap = self.obj.backend
|
|
members = []
|
|
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']
|
|
if names:
|
|
for name in names:
|
|
if not name:
|
|
continue
|
|
name = normalize_principal(name)
|
|
obj_dn = ldap_obj.get_dn(name)
|
|
try:
|
|
ldap.get_entry(obj_dn, ['krbprincipalname'])
|
|
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)
|
|
else:
|
|
raise errors.AlreadyGroupMember()
|
|
except errors.PublicError as e:
|
|
failed[self.principal_failedattr][
|
|
self.principal_attr].append((name, unicode(e)))
|
|
else:
|
|
completed += 1
|
|
|
|
if members:
|
|
value = entry_attrs.setdefault(self.principal_attr, [])
|
|
value.extend(members)
|
|
|
|
try:
|
|
ldap.update_entry(entry_attrs)
|
|
except errors.EmptyModlist:
|
|
pass
|
|
|
|
return completed, dn
|
|
|
|
|
|
class servicedelegation_remove_member(LDAPRemoveMember):
|
|
__doc__ = _('Remove member from a named service delegation.')
|
|
|
|
member_attrs = ['memberprincipal']
|
|
member_attributes = []
|
|
member_names = {}
|
|
principal_attr = 'memberprincipal'
|
|
principal_failedattr = 'failed_memberprincipal'
|
|
|
|
def get_options(self):
|
|
for option in super(
|
|
servicedelegation_remove_member, self).get_options():
|
|
yield option
|
|
for attr in self.member_attrs:
|
|
name = self.member_names[attr]
|
|
doc = self.member_param_doc % name
|
|
yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
|
|
label=_('member %s') % name, alwaysask=True)
|
|
|
|
def get_member_dns(self, **options):
|
|
"""
|
|
Need to ignore memberPrincipal for now and handle the difference
|
|
in objectclass between a rule and a target.
|
|
"""
|
|
dns = {}
|
|
failed = {}
|
|
for attr in self.member_attrs:
|
|
dns[attr] = {}
|
|
if attr.lower() == 'memberprincipal':
|
|
# This will be handled later. memberprincipal isn't a
|
|
# DN so will blow up in assertions in baseldap.
|
|
continue
|
|
failed[attr] = {}
|
|
for ldap_obj_name in self.obj.attribute_members[attr]:
|
|
dns[attr][ldap_obj_name] = []
|
|
failed[attr][ldap_obj_name] = []
|
|
names = options.get(self.member_names[attr], [])
|
|
if not names:
|
|
continue
|
|
for name in names:
|
|
if not name:
|
|
continue
|
|
ldap_obj = self.api.Object[ldap_obj_name]
|
|
try:
|
|
dns[attr][ldap_obj_name].append(ldap_obj.get_dn(name))
|
|
except errors.PublicError as e:
|
|
failed[attr][ldap_obj_name].append((name, unicode(e)))
|
|
return dns, failed
|
|
|
|
def post_callback(self, ldap, completed, failed, dn, entry_attrs,
|
|
*keys, **options):
|
|
"""
|
|
Remove memberPrincipal values. This is done afterward because it
|
|
isn't a DN and the LDAPAddMember method explicitly only handles DNs.
|
|
|
|
See servicedelegation_add_member() for an explanation of what
|
|
failedattr is.
|
|
"""
|
|
ldap = self.obj.backend
|
|
failed[self.principal_failedattr] = {}
|
|
failed[self.principal_failedattr][self.principal_attr] = []
|
|
names = options.get(self.member_names[self.principal_attr], [])
|
|
if names:
|
|
for name in names:
|
|
if not name:
|
|
continue
|
|
name = normalize_principal(name)
|
|
try:
|
|
if name in entry_attrs.get(self.principal_attr, []):
|
|
entry_attrs[self.principal_attr].remove(name)
|
|
else:
|
|
raise errors.NotGroupMember()
|
|
except errors.PublicError as e:
|
|
failed[self.principal_failedattr][
|
|
self.principal_attr].append((name, unicode(e)))
|
|
else:
|
|
completed += 1
|
|
|
|
try:
|
|
ldap.update_entry(entry_attrs)
|
|
except errors.EmptyModlist:
|
|
pass
|
|
|
|
return completed, dn
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule(servicedelegation):
|
|
"""
|
|
A service delegation rule. This is the ACL that controls
|
|
what can be delegated to whom.
|
|
"""
|
|
object_name = _('service delegation rule')
|
|
object_name_plural = _('service delegation rules')
|
|
object_class = ['ipakrb5delegationacl', 'groupofprincipals', 'top']
|
|
default_attributes = [
|
|
'cn', 'memberprincipal', 'ipaallowedtarget',
|
|
'ipaallowedtoimpersonate',
|
|
]
|
|
attribute_members = {
|
|
# memberprincipal is not listed because it isn't a DN
|
|
'ipaallowedtarget': ['servicedelegationtarget'],
|
|
}
|
|
|
|
label = _('Service delegation rules')
|
|
label_singular = _('Service delegation rule')
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule_add(LDAPCreate):
|
|
__doc__ = _('Create a new service delegation rule.')
|
|
|
|
msg_summary = _('Added service delegation rule "%(value)s"')
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule_del(LDAPDelete):
|
|
__doc__ = _('Delete service delegation.')
|
|
|
|
msg_summary = _('Deleted service delegation "%(value)s"')
|
|
|
|
def pre_callback(self, ldap, dn, *keys, **options):
|
|
assert isinstance(dn, DN)
|
|
if keys[0] in PROTECTED_CONSTRAINT_RULES:
|
|
raise errors.ProtectedEntryError(
|
|
label=_(u'service delegation rule'),
|
|
key=keys[0],
|
|
reason=_(u'privileged service delegation rule')
|
|
)
|
|
return dn
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule_find(LDAPSearch):
|
|
__doc__ = _('Search for service delegations rule.')
|
|
|
|
msg_summary = ngettext(
|
|
'%(count)d service delegation rule matched',
|
|
'%(count)d service delegation rules matched', 0
|
|
)
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule_show(LDAPRetrieve):
|
|
__doc__ = _('Display information about a named service delegation rule.')
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule_add_member(servicedelegation_add_member):
|
|
__doc__ = _('Add member to a named service delegation rule.')
|
|
|
|
member_names = {
|
|
'memberprincipal': 'principal',
|
|
}
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule_remove_member(servicedelegation_remove_member):
|
|
__doc__ = _('Remove member from a named service delegation rule.')
|
|
member_names = {
|
|
'memberprincipal': 'principal',
|
|
}
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule_add_target(LDAPAddMember):
|
|
__doc__ = _('Add target to a named service delegation rule.')
|
|
|
|
member_attributes = ['ipaallowedtarget']
|
|
attribute_members = {
|
|
'ipaallowedtarget': ['servicedelegationtarget'],
|
|
}
|
|
|
|
|
|
@register()
|
|
class servicedelegationrule_remove_target(LDAPRemoveMember):
|
|
__doc__ = _('Remove target from a named service delegation rule.')
|
|
member_attributes = ['ipaallowedtarget']
|
|
attribute_members = {
|
|
'ipaallowedtarget': ['servicedelegationtarget'],
|
|
}
|
|
|
|
|
|
@register()
|
|
class servicedelegationtarget(servicedelegation):
|
|
object_name = _('service delegation target')
|
|
object_name_plural = _('service delegation targets')
|
|
object_class = ['groupofprincipals', 'top']
|
|
default_attributes = [
|
|
'cn', 'memberprincipal',
|
|
]
|
|
attribute_members = {}
|
|
|
|
label = _('Service delegation targets')
|
|
label_singular = _('Service delegation target')
|
|
|
|
|
|
@register()
|
|
class servicedelegationtarget_add(LDAPCreate):
|
|
__doc__ = _('Create a new service delegation target.')
|
|
|
|
msg_summary = _('Added service delegation target "%(value)s"')
|
|
|
|
|
|
@register()
|
|
class servicedelegationtarget_del(LDAPDelete):
|
|
__doc__ = _('Delete service delegation target.')
|
|
|
|
msg_summary = _('Deleted service delegation target "%(value)s"')
|
|
|
|
def pre_callback(self, ldap, dn, *keys, **options):
|
|
assert isinstance(dn, DN)
|
|
if keys[0] in PROTECTED_CONSTRAINT_TARGETS:
|
|
raise errors.ProtectedEntryError(
|
|
label=_(u'service delegation target'),
|
|
key=keys[0],
|
|
reason=_(u'privileged service delegation target')
|
|
)
|
|
return dn
|
|
|
|
|
|
@register()
|
|
class servicedelegationtarget_find(LDAPSearch):
|
|
__doc__ = _('Search for service delegation target.')
|
|
|
|
msg_summary = ngettext(
|
|
'%(count)d service delegation target matched',
|
|
'%(count)d service delegation targets matched', 0
|
|
)
|
|
|
|
def pre_callback(self, ldap, filters, attrs_list, base_dn, scope,
|
|
term=None, **options):
|
|
"""
|
|
Exclude rules from the search output. A target contains a subset
|
|
of a rule objectclass.
|
|
"""
|
|
search_kw = self.args_options_2_entry(**options)
|
|
search_kw['objectclass'] = self.obj.object_class
|
|
attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL)
|
|
rule_kw = {'objectclass': 'ipakrb5delegationacl'}
|
|
target_filter = ldap.make_filter(rule_kw, rules=ldap.MATCH_NONE)
|
|
attr_filter = ldap.combine_filters(
|
|
(target_filter, attr_filter), rules=ldap.MATCH_ALL
|
|
)
|
|
|
|
search_kw = {}
|
|
for a in self.obj.default_attributes:
|
|
search_kw[a] = term
|
|
|
|
term_filter = ldap.make_filter(search_kw, exact=False)
|
|
|
|
sfilter = ldap.combine_filters(
|
|
(term_filter, attr_filter), rules=ldap.MATCH_ALL
|
|
)
|
|
return sfilter, base_dn, ldap.SCOPE_ONELEVEL
|
|
|
|
|
|
@register()
|
|
class servicedelegationtarget_show(LDAPRetrieve):
|
|
__doc__ = _('Display information about a named service delegation target.')
|
|
|
|
|
|
@register()
|
|
class servicedelegationtarget_add_member(servicedelegation_add_member):
|
|
__doc__ = _('Add member to a named service delegation target.')
|
|
|
|
member_names = {
|
|
'memberprincipal': 'principal',
|
|
}
|
|
|
|
|
|
@register()
|
|
class servicedelegationtarget_remove_member(servicedelegation_remove_member):
|
|
__doc__ = _('Remove member from a named service delegation target.')
|
|
member_names = {
|
|
'memberprincipal': 'principal',
|
|
}
|