summaryrefslogtreecommitdiffstats
path: root/ipalib/plugins
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2015-05-14 13:08:58 +0000
committerJan Cholasta <jcholast@redhat.com>2015-06-03 09:47:40 +0000
commita92328452dced34d6d6df7ad6fe585563bb909f6 (patch)
tree19d7455b17463f411e0f0ac7cbb94517cb6bc214 /ipalib/plugins
parent7f7c247bb5a4b0030d531f4f14c156162e808212 (diff)
downloadfreeipa-a92328452dced34d6d6df7ad6fe585563bb909f6.tar.gz
freeipa-a92328452dced34d6d6df7ad6fe585563bb909f6.tar.xz
freeipa-a92328452dced34d6d6df7ad6fe585563bb909f6.zip
Add plugin to manage service constraint delegations
Service Constraints are the delegation model used by ipa-kdb to grant service A to obtain a TGT for a user against service B. https://fedorahosted.org/freeipa/ticket/3644 Reviewed-By: Martin Basti <mbasti@redhat.com>
Diffstat (limited to 'ipalib/plugins')
-rw-r--r--ipalib/plugins/servicedelegation.py537
1 files changed, 537 insertions, 0 deletions
diff --git a/ipalib/plugins/servicedelegation.py b/ipalib/plugins/servicedelegation.py
new file mode 100644
index 000000000..34312e39a
--- /dev/null
+++ b/ipalib/plugins/servicedelegation.py
@@ -0,0 +1,537 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+from ipalib import api
+from ipalib import Str
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import *
+from ipalib.plugins.service import normalize_principal
+from ipalib import _, ngettext
+
+__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 ablity
+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',
+
+)
+
+
+output_params = (
+ Str(
+ 'ipaallowedtarget_servicedelegationtarget',
+ label=_('Allowed Target'),
+ ),
+ Str(
+ 'ipaallowedtoimpersonate',
+ label=_('Allowed to Impersonate'),
+ ),
+ Str(
+ 'memberprincipal',
+ label=_('Member principals'),
+ ),
+ Str(
+ 'failed_memberprincipal',
+ label=_('Failed members'),
+ ),
+ Str(
+ 'ipaallowedtarget',
+ label=_('Failed 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'},
+ },
+ }
+
+ rdn_is_primary_key = True
+
+ takes_params = (
+ Str(
+ 'cn',
+ pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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,
+ ),
+ )
+
+
+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'
+
+ has_output_params = LDAPAddMember.has_output_params + output_params
+
+ 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'
+
+ has_output_params = LDAPRemoveMember.has_output_params + output_params
+
+ 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.')
+
+ has_output_params = LDAPSearch.has_output_params + output_params
+
+ 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.')
+
+ has_output_params = LDAPRetrieve.has_output_params + output_params
+
+
+@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'],
+ }
+ has_output_params = LDAPAddMember.has_output_params + output_params
+
+
+@register()
+class servicedelegationrule_remove_target(LDAPRemoveMember):
+ __doc__ = _('Remove target from a named service delegation rule.')
+ member_attributes = ['ipaallowedtarget']
+ attribute_members = {
+ 'ipaallowedtarget': ['servicedelegationtarget'],
+ }
+ has_output_params = LDAPRemoveMember.has_output_params + output_params
+
+
+@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.')
+
+ has_output_params = LDAPSearch.has_output_params + output_params
+
+ 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,
+ *args, **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 = {}
+ term = args[-1]
+ 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.')
+
+ has_output_params = LDAPRetrieve.has_output_params + output_params
+
+
+@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',
+ }