diff options
author | Rob Crittenden <rcritten@redhat.com> | 2015-05-14 13:08:58 +0000 |
---|---|---|
committer | Jan Cholasta <jcholast@redhat.com> | 2015-06-03 09:47:40 +0000 |
commit | a92328452dced34d6d6df7ad6fe585563bb909f6 (patch) | |
tree | 19d7455b17463f411e0f0ac7cbb94517cb6bc214 /ipalib | |
parent | 7f7c247bb5a4b0030d531f4f14c156162e808212 (diff) | |
download | freeipa-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')
-rw-r--r-- | ipalib/plugins/servicedelegation.py | 537 |
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', + } |