Rob Crittenden wrote:
Add a plugin to manage service delegations, like the one allowing the
HTTP service to obtain an ldap service ticket on behalf of the user.

This does not include impersonation targets, so one cannot yet limit by
user what tickets can be obtained.

There is also no referential integrity for the memberPrincipal attribute
since it is a string and not a DN. I don't see a way around this that
isn't either clunky or requires a 389-ds plugin, both of which are
overkill in this case IMHO.

If you wonder why all the overrides it's because all of this is stored
in the same container, and membership-like functions are used for a
non-DN attribute (memberPrincipal).

I used Alexander's patch in the ticket as a jumping off point.

Removed a couple of hardcoded domain/realm elements in the tests.

rob

>From b1366069b4494cac38d486d9dda02a03226fd0d3 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Thu, 14 May 2015 13:08:58 +0000
Subject: [PATCH] Add plugin to manage service constraints

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
---
 API.txt                                            |  72 ++++
 VERSION                                            |   4 +-
 install/updates/20-indices.update                  |   9 +
 install/updates/25-referint.update                 |   1 +
 ipalib/plugins/serviceconstraint.py                | 444 +++++++++++++++++++
 ipatests/test_xmlrpc/objectclasses.py              |  11 +
 .../test_xmlrpc/test_serviceconstraint_plugin.py   | 479 +++++++++++++++++++++
 7 files changed, 1018 insertions(+), 2 deletions(-)
 create mode 100644 ipalib/plugins/serviceconstraint.py
 create mode 100644 ipatests/test_xmlrpc/test_serviceconstraint_plugin.py

diff --git a/API.txt b/API.txt
index 0808f3c64595495c8a9e60da5cbd689d5cdc6224..b548132a1e119204cd8452c4b8db80fa00263ccc 100644
--- a/API.txt
+++ b/API.txt
@@ -3694,6 +3694,78 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: serviceconstraint_add
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='constraint_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][ a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.-]?$', primary_key=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: StrEnum('type', values=(u'rule', u'target'))
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: serviceconstraint_add_member
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='constraint_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][ a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('principal*', alwaysask=True, cli_name='principals', csv=True)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('target*', alwaysask=True, cli_name='targets', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: serviceconstraint_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='constraint_name', maxlength=255, multivalue=True, pattern='^[a-zA-Z0-9_.][ a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.-]?$', primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: serviceconstraint_find
+args: 1,9,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='constraint_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][ a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.-]?$', primary_key=True, query=True, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: StrEnum('type', values=(u'rule', u'target'))
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: serviceconstraint_remove_member
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='constraint_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][ a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('principal*', alwaysask=True, cli_name='principals', csv=True)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('target*', alwaysask=True, cli_name='targets', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: serviceconstraint_show
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='constraint_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][ a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: sidgen_was_run
 args: 0,1,1
 option: Str('version?', exclude='webui')
diff --git a/VERSION b/VERSION
index c207558504e645dec73d105189d4b862877b4e26..0bbdad84aa94d8d8ae92884befea9f9a7ce9b571 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=118
-# Last change: tbordaz - Add stageuser_find, stageuser_mod, stageuser_del, stageuser_show
+IPA_API_VERSION_MINOR=119
+# Last change: rcritten - added service constraint delegation plugin
diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update
index e6e4888e2eda1848b636183528cbf90e22384ea9..880e73f3bb1b2a32c2fa40f65666cfd594cdc659 100644
--- a/install/updates/20-indices.update
+++ b/install/updates/20-indices.update
@@ -182,3 +182,12 @@ default:nsSystemIndex: false
 only:nsIndexType: eq
 only:nsIndexType: pres
 only:nsIndexType: sub
+
+dn: cn=ipaallowedtarget,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+default:cn: ipaallowedtarget
+default:ObjectClass: top
+default:ObjectClass: nsIndex
+default:nsSystemIndex: false
+only:nsIndexType: eq
+only:nsIndexType: pres
+only:nsIndexType: sub
diff --git a/install/updates/25-referint.update b/install/updates/25-referint.update
index 609eaba74f0fcde6ce875093587315681fbd4584..005cd0376d82c83b1b7ab368f992e209b0da5e9a 100644
--- a/install/updates/25-referint.update
+++ b/install/updates/25-referint.update
@@ -16,3 +16,4 @@ add: referint-membership-attr: ipasudorunas
 add: referint-membership-attr: ipasudorunasgroup
 add: referint-membership-attr: ipatokenradiusconfiglink
 add: referint-membership-attr: ipaassignedidview
+add: referint-membership-attr: ipaallowedtarget
diff --git a/ipalib/plugins/serviceconstraint.py b/ipalib/plugins/serviceconstraint.py
new file mode 100644
index 0000000000000000000000000000000000000000..553a52e64ee58360065b994fcc0c5973246d915d
--- /dev/null
+++ b/ipalib/plugins/serviceconstraint.py
@@ -0,0 +1,444 @@
+# Author:
+#   Alexander Bokovoy <aboko...@redhat.com>
+#   Rob Crittenden <rcrit...@redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib import api
+from ipalib import Str, StrEnum
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import *
+from ipalib.plugins import baseldap
+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 delegataion rule:
+   ipa serviceconstraint-add --type=rule ftp-delegation
+
+ Add a new constrained delegation target:
+   ipa serviceconstraint-add --type=target ftp-delegation-target
+
+ Add a principal to the rule:
+   ipa serviceconstraint-add-member --prinicpals=ftp/ipa.example.com \
+      ftp-delegation
+
+ Add our target to the rule:
+   ipa serviceconstraint-add-member --targets=ftp-delegation-target \
+      ftp-delegation
+
+ Add a principal to the target
+   ipa serviceconstraint-add-member --prinicpals=ldap/ipa.example.com \
+      ftp-delegation-target
+
+ Display information about a named group.
+   ipa serviceconstraint-show ftp-delegation
+
+ Remove a constrained delegation:
+   ipa serviceconstraint-del ftp-delegation-target
+   ipa serviceconstraint-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_CONSTRAINTS = (
+    u'ipa-cifs-delegation-targets',
+    u'ipa-ldap-delegation-targets',
+    u'ipa-http-delegation',
+)
+
+output_params = (
+    Str('ipaallowedtarget_serviceconstraint',
+        label=_('Allowed Target'),
+    ),
+    Str('ipaallowedtoimpersonate',
+        label=_('Allowed to Impersonate'),
+    ),
+    Str('memberprincipal',
+        label=_('Member'),
+    ),
+    Str('ipaallowedtarget',
+        label=_('Failed targets'),
+    ),
+    Str('serviceconstraint',
+        label=_('principal member'),
+    ),
+)
+
+
+constraint_type = (
+    StrEnum('type',
+        label=_('Constraint Type'),
+        doc=_('Type of constraint'),
+        values=(u'rule', u'target', ),
+    ),
+)
+
+
+@register()
+class serviceconstraint(LDAPObject):
+    """
+    Service Constrained Delegation 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_name = _('service constraint')
+    object_name_plural = _('service constraints')
+    object_class = ['groupofprincipals', 'top']
+    default_attributes = [
+        'cn', 'memberprincipal', 'ipaallowedtarget',
+        'ipaallowedtoimpersonate',
+    ]
+    attribute_members = {
+        # memberprincipal is not listed because it isn't a DN
+        'ipaallowedtarget': ['serviceconstraint'],
+        'ipaallowedtoimpersonate': ['serviceconstraint'],
+    }
+
+    rdn_is_primary_key = True
+
+    label = _('Service constraints')
+    label_singular = _('Service constraint ruile')
+
+    takes_params = (
+        Str('cn',
+            pattern='^[a-zA-Z0-9_.][ a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.-]?$',
+            pattern_errmsg='may only include letters, numbers, _, -, ., and a space inside',
+            maxlength=255,
+            cli_name='constraint_name',
+            label=_('Constraint name'),
+            primary_key=True,
+        ),
+    )
+
+
+@register()
+class serviceconstraint_add(LDAPCreate):
+    __doc__ = _('Create a new service constraint.')
+
+    msg_summary = _('Added service constraint "%(value)s"')
+
+    takes_options = LDAPCreate.takes_options + constraint_type
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list,
+                     *keys, **options):
+        assert isinstance(dn, DN)
+        if options.get('type') == u'rule':
+            entry_attrs['objectclass'].append('ipakrb5delegationacl')
+        return dn
+
+
+@register()
+class serviceconstraint_del(LDAPDelete):
+    __doc__ = _('Delete service constraint.')
+
+    msg_summary = _('Deleted service constraint "%(value)s"')
+
+    def pre_callback(self, ldap, dn, *keys, **options):
+        assert isinstance(dn, DN)
+        constraint_attrs = self.obj.methods.show(
+            self.obj.get_primary_key_from_dn(dn), all=True
+        )['result']
+        if keys[0] in PROTECTED_CONSTRAINTS:
+            raise errors.ProtectedEntryError(
+                label=_(u'service constraint'),
+                key=keys[0],
+                reason=_(u'privileged service constraint')
+            )
+        return dn
+
+
+@register()
+class serviceconstraint_find(LDAPSearch):
+    __doc__ = _('Search for service constraints.')
+
+    takes_options = LDAPSearch.takes_options + constraint_type
+    has_output_params = LDAPSearch.has_output_params + output_params
+
+    msg_summary = ngettext(
+        '%(count)d service constraints matched', '%(count)d service constraints matched', 0
+    )
+
+    def pre_callback(self, ldap, filters, attrs_list, base_dn, scope,
+                     *args, **options):
+        # Integrate type into the search filter
+        search_kw = self.args_options_2_entry(**options)
+        if options.get('type') == u'rule':
+            search_kw['objectclass'] = ['ipakrb5delegationacl']
+            attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL)
+        else:
+            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 serviceconstraint_show(LDAPRetrieve):
+    __doc__ = _('Display information about a named service constraint.')
+
+    has_output_params = LDAPRetrieve.has_output_params + output_params
+
+
+@register()
+class serviceconstraint_add_member(LDAPAddMember):
+    __doc__ = _('Add target to a named service constraint.')
+    member_attrs = ['ipaallowedtarget', 'memberprincipal']
+    member_attributes = []
+    member_names = {
+        'ipaallowedtarget': 'target',
+        'memberprincipal': 'principal',
+    }
+    has_output_params = LDAPSearch.has_output_params + output_params
+
+    def get_options(self):
+        for option in super(serviceconstraint_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,
+                      csv=True, 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.
+        """
+        ldap = self.obj.backend
+        dns = {}
+        failed = {}
+        for attr in self.member_attrs:
+            dns[attr] = {}
+            failed[attr] = {}
+            if attr.lower() == 'memberprincipal':
+                # This will be handled later. memberprincipal isn't a
+                # DN so will blow up in assertions in baseldap.
+                continue
+            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]
+                    obj_dn = ldap_obj.get_dn(name)
+                    try:
+                        target_attrs = ldap.get_entry(obj_dn, ['objectclass'])
+                    except errors.PublicError, e:
+                        dns[attr][ldap_obj_name].append(obj_dn)
+                        continue
+                    if self.obj.has_objectclass(target_attrs['objectclass'],
+                                                'ipakrb5delegationacl'):
+                        failed[attr][ldap_obj_name].append(
+                            (name, u'Cannot add a rule as a target')
+                        )
+                        continue
+                    try:
+                        dns[attr][ldap_obj_name].append(obj_dn)
+                    except errors.PublicError, 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):
+        """
+        Add memberPrincipal values. This is done afterward because it isn't
+        a DN and the LDAPAddMember method explicitly only handles DNs.
+        """
+        ldap = self.obj.backend
+        ldap_obj_name = self.obj.name
+        attr = 'memberprincipal'
+        members = []
+        failed[attr] = {}
+        failed[attr][ldap_obj_name] = []
+        names = options.get(self.member_names[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:
+                    s_attrs = ldap.get_entry(obj_dn, ['krbprincipalname'])
+                except errors.NotFound, e:
+                    failed[attr][ldap_obj_name].append((name, unicode(e)))
+                    continue
+                try:
+                    if name not in entry_attrs.get(attr, []):
+                        members.append(name)
+                    else:
+                        raise errors.AlreadyGroupMember()
+                except errors.PublicError, e:
+                    failed[attr][ldap_obj_name].append((name, unicode(e)))
+                else:
+                    completed += 1
+
+        if members:
+            if attr in entry_attrs:
+                entry_attrs[attr] += members
+            else:
+                entry_attrs[attr] = members
+            try:
+                ldap.update_entry(entry_attrs)
+            except errors.EmptyModlist:
+                pass
+
+        return (completed, dn)
+
+
+@register()
+class serviceconstraint_remove_member(LDAPRemoveMember):
+    __doc__ = _('Remove target to a named service constraint.')
+    member_attrs = ['ipaallowedtarget', 'memberprincipal']
+    member_attributes = []
+    member_names = {
+        'ipaallowedtarget': 'target',
+        'memberprincipal': 'principal',
+    }
+    has_output_params = LDAPSearch.has_output_params + output_params
+
+    def get_options(self):
+        for option in super(serviceconstraint_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,
+                      csv=True, 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] = {}
+            failed[attr] = {}
+            if attr.lower() == 'memberprincipal':
+                # This will be handled later. memberprincipal isn't a
+                # DN so will blow up in assertions in baseldap.
+                continue
+            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, 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.
+        """
+        ldap = self.obj.backend
+        ldap_obj_name = self.obj.name
+        attr = 'memberprincipal'
+        members = []
+        failed[attr][ldap_obj_name] = []
+        names = options.get(self.member_names[attr], [])
+        if names:
+            for name in names:
+                if not name:
+                    continue
+                name = normalize_principal(name)
+                try:
+                    if name in entry_attrs.get(attr, []):
+                        entry_attrs[attr].remove(name)
+                    else:
+                        raise errors.NotGroupMember()
+                except errors.PublicError, e:
+                    failed[attr][ldap_obj_name].append((name, unicode(e)))
+                else:
+                    completed += 1
+
+        try:
+            ldap.update_entry(entry_attrs)
+        except errors.EmptyModlist:
+            pass
+
+        return (completed, dn)
diff --git a/ipatests/test_xmlrpc/objectclasses.py b/ipatests/test_xmlrpc/objectclasses.py
index 9a69cf3fdee65eff2f8eaba03272104635f3afd5..f1cdec47daa0827145b84d74221ea2c0b8c0caf7 100644
--- a/ipatests/test_xmlrpc/objectclasses.py
+++ b/ipatests/test_xmlrpc/objectclasses.py
@@ -201,3 +201,14 @@ idoverridegroup = [
     u'top',
     u'ipaGroupOverride',
 ]
+
+serviceconstraint_rule = [
+    u'top',
+    u'groupofprincipals',
+    u'ipakrb5delegationacl',
+]
+
+serviceconstraint_target = [
+    u'top',
+    u'groupofprincipals',
+]
diff --git a/ipatests/test_xmlrpc/test_serviceconstraint_plugin.py b/ipatests/test_xmlrpc/test_serviceconstraint_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..9dc0d0aefa2522af262fd2a50dc6b2db662e8b76
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_serviceconstraint_plugin.py
@@ -0,0 +1,479 @@
+# Authors:
+#   Rob Crittenden <rcrit...@redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Test the `ipalib/plugins/serviceconstraint.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import (Declarative, fuzzy_digits, fuzzy_uuid, fuzzy_set_ci,
+                         add_sid, add_oc)
+from ipapython.dn import DN
+from ipatests.test_xmlrpc.test_user_plugin import get_user_result
+
+rule1 = u'test1'
+rule2 = u'test2'
+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)
+
+invalidrule1=u'+rule1'
+
+def get_serviceconstraint_dn(cn):
+    return DN(('cn', cn), api.env.container_s4u2proxy, api.env.basedn)
+
+class test_serviceconstraint(Declarative):
+    cleanup_commands = [
+        ('serviceconstraint_del', [rule1], {}),
+        ('serviceconstraint_del', [rule2], {}),
+        ('serviceconstraint_del', [target1], {}),
+        ('serviceconstraint_del', [target2], {}),
+    ]
+
+    tests = [
+
+        ################
+        # create rule1:
+        dict(
+            desc='Try to retrieve non-existent %r' % rule1,
+            command=('serviceconstraint_show', [rule1], {}),
+            expected=errors.NotFound(reason=u'%s: service constraint not found' % rule1),
+        ),
+
+
+        dict(
+            desc='Try to delete non-existent %r' % rule1,
+            command=('serviceconstraint_del', [rule1], {}),
+            expected=errors.NotFound(reason=u'%s: service constraint not found' % rule1),
+        ),
+
+
+        dict(
+            desc='Create %r' % rule1,
+            command=(
+                'serviceconstraint_add', [rule1], {'type': u'rule'}
+            ),
+            expected=dict(
+                value=rule1,
+                summary=u'Added service constraint "%s"' % rule1,
+                result=dict(
+                    cn=[rule1],
+                    objectclass=objectclasses.serviceconstraint_rule,
+                    dn=get_serviceconstraint_dn(rule1),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Try to create duplicate %r' % rule1,
+            command=(
+                'serviceconstraint_add', [rule1], {'type': u'rule'}
+            ),
+            expected=errors.DuplicateEntry(
+                message=u'service constraint with name "%s" already exists' % rule1),
+        ),
+
+
+        dict(
+            desc='Retrieve %r' % rule1,
+            command=('serviceconstraint_show', [rule1], {}),
+            expected=dict(
+                value=rule1,
+                summary=None,
+                result=dict(
+                    cn=[rule1],
+                    dn=get_serviceconstraint_dn(rule1),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Search for %r' % rule1,
+            command=('serviceconstraint_find', [], dict(cn=rule1, type=u'rule')),
+            expected=dict(
+                count=1,
+                truncated=False,
+                result=[
+                    dict(
+                        dn=get_serviceconstraint_dn(rule1),
+                        cn=[rule1],
+                    ),
+                ],
+                summary=u'1 service constraints matched',
+            ),
+        ),
+
+
+
+        ################
+        # create rule2:
+        dict(
+            desc='Create %r' % rule2,
+            command=(
+                'serviceconstraint_add', [rule2], {'type': u'rule'}
+            ),
+            expected=dict(
+                value=rule2,
+                summary=u'Added service constraint "%s"' % rule2,
+                result=dict(
+                    cn=[rule2],
+                    objectclass=objectclasses.serviceconstraint_rule,
+                    dn=get_serviceconstraint_dn(rule2),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Search for all rules',
+            command=('serviceconstraint_find', [], {'type': u'rule'}),
+            expected=dict(
+                summary=u'3 service constraints matched',
+                count=3,
+                truncated=False,
+                result=[
+                    {
+                        'dn': get_serviceconstraint_dn(u'ipa-http-delegation'),
+                        'cn': [u'ipa-http-delegation'],
+                        'memberprincipal': [princ1],
+                        'ipaallowedtarget_serviceconstraint':
+                            [u'ipa-ldap-delegation-targets',
+                             u'ipa-cifs-delegation-targets']
+                    },
+                    dict(
+                        dn=get_serviceconstraint_dn(rule1),
+                        cn=[rule1],
+                    ),
+                    dict(
+                        dn=get_serviceconstraint_dn(rule2),
+                        cn=[rule2],
+                    ),
+                ],
+            ),
+        ),
+
+
+        dict(
+            desc='Create target %r' % target1,
+            command=(
+                'serviceconstraint_add', [target1], {'type': u'target'}
+            ),
+            expected=dict(
+                value=target1,
+                summary=u'Added service constraint "%s"' % target1,
+                result=dict(
+                    cn=[target1],
+                    objectclass=objectclasses.serviceconstraint_target,
+                    dn=get_serviceconstraint_dn(target1),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Create target %r' % target2,
+            command=(
+                'serviceconstraint_add', [target2], {'type': u'target'}
+            ),
+            expected=dict(
+                value=target2,
+                summary=u'Added service constraint "%s"' % target2,
+                result=dict(
+                    cn=[target2],
+                    objectclass=objectclasses.serviceconstraint_target,
+                    dn=get_serviceconstraint_dn(target2),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Search for all targets',
+            command=('serviceconstraint_find', [], {'type': u'target'}),
+            expected=dict(
+                summary=u'4 service constraints matched',
+                count=4,
+                truncated=False,
+                result=[
+                    {
+                        'dn': get_serviceconstraint_dn(u'ipa-cifs-delegation-targets'),
+                        'cn': [u'ipa-cifs-delegation-targets'],
+                    },
+                    {
+                        'dn': get_serviceconstraint_dn(u'ipa-ldap-delegation-targets'),
+                        'cn': [u'ipa-ldap-delegation-targets'],
+                        'memberprincipal': [princ2],
+                    },
+                    dict(
+                        dn=get_serviceconstraint_dn(target1),
+                        cn=[target1],
+                    ),
+                    dict(
+                        dn=get_serviceconstraint_dn(target2),
+                        cn=[target2],
+                    ),
+                ],
+            ),
+        ),
+
+
+        ###############
+        # target member stuff:
+        dict(
+            desc='Add member %r to %r' % (target1, rule1),
+            command=(
+                'serviceconstraint_add_member', [rule1], dict(target=target1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'ipaallowedtarget_serviceconstraint': (target1,),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add duplicate member %r to %r' % (target1, rule1),
+            command=(
+                'serviceconstraint_add_member', [rule1], dict(target=target1)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=[[target1, u'This entry is already a member']],
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'ipaallowedtarget_serviceconstraint': (target1,),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add non-existent member %r to %r' % (u'notfound', rule1),
+            command=(
+                'serviceconstraint_add_member', [rule1], dict(target=u'notfound')
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=[[u'notfound', u'no such entry']],
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'ipaallowedtarget_serviceconstraint': (target1,),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove a member %r from %r' % (target1, rule1),
+            command=(
+                'serviceconstraint_remove_member', [rule1], dict(target=target1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove non-existent member %r from %r' % (u'notfound', rule1),
+            command=(
+                'serviceconstraint_remove_member', [rule1], dict(target=u'notfound')
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=[[u'notfound', u'This entry is not a member']],
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        ###############
+        # memberprincipal member stuff:
+        dict(
+            desc='Add memberprinc %r to %r' % (princ1, rule1),
+            command=(
+                'serviceconstraint_add_member', [rule1], dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'memberprincipal': (princ1,),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add duplicate member %r to %r' % (princ1, rule1),
+            command=(
+                'serviceconstraint_add_member', [rule1], dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=[[princ1, u'This entry is already a member']],
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'memberprincipal': (princ1,),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add non-existent member %r to %r' % (u'HTTP/notfound', rule1),
+            command=(
+                'serviceconstraint_add_member', [rule1], dict(principal=u'HTTP/notfound@%s' % api.env.realm)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=[[u'HTTP/notfound@%s' % api.env.realm, u'no such entry']],
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'memberprincipal': (princ1,),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove a member %r from %r' % (princ1, rule1),
+            command=(
+                'serviceconstraint_remove_member', [rule1], dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'memberprincipal': [],
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove non-existent member %r from %r' % (u'HTTP/notfound', rule1),
+            command=(
+                'serviceconstraint_remove_member', [rule1], dict(principal=u'HTTP/notfound@%s' % api.env.realm)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        serviceconstraint=tuple(),
+                    ),
+                    memberprincipal=dict(
+                        serviceconstraint=[[u'HTTP/notfound@%s' % api.env.realm, u'This entry is not a member']],
+                    ),
+                ),
+                result={
+                        'dn': get_serviceconstraint_dn(rule1),
+                        'cn': [rule1],
+                },
+            ),
+        ),
+
+    ]
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to