Martin Basti wrote:
On 31/05/15 04:07, Rob Crittenden wrote:
Petr Vobornik wrote:
On 05/27/2015 08:17 PM, Martin Basti wrote:
On 27/05/15 19:27, Rob Crittenden wrote:
Martin Basti wrote:


Thank you.

I haven't finished review yet, but I have few notes in case you will
modify the patch.

Please fix following issues:


3)
There are many PEP8 errors, can you fix some of them,?

Is PEP8 a concern? What kinds of errors do we fix? For example, the
current model for defining options generates a slew of indention
errors.

In old modules it's preferred to keep the old indentation style for
options(not to mix 2 styles). New modules should use following pep8
compliant style:
         Str(
             'cn',
             cli_name='name',
             primary_key=True,
             label=_('Server name'),
             doc=_('IPA server hostname'),
         ),

We try to keep PEP8 in new code, mainly indentation, blank lines, too
long lines.
Yes in test definitions and option definitions, is better to keep the
same style, but other parts of code should be PEP8.

For example these should be fixed
./ipatests/test_xmlrpc/test_serviceconstraint_plugin.py:37:13: E225
missing whitespace around operator
./ipatests/test_xmlrpc/test_serviceconstraint_plugin.py:39:1: E302
expected 2 blank lines, found 1
./ipatests/test_xmlrpc/test_serviceconstraint_plugin.py:42:1: E302
expected 2 blank lines, found 1



I'll wait and see what falls out of the API review before making any
real changes.

rob

Updated API and addressed Martin's concerns. The regex must have been
a bad copy/paste, it is fixed now.

The design page has been updated as well.

rob

Hello,

comments below, in the right thread:

1)
+    Str(
+        'memberprincipal',
+        label=_('Failed principals'),
+    ),
+    Str(
+        'ipaallowedtarget',
+        label=_('Failed targets'),
+    ),
+    Str(
+        'servicedelegationrule',
+        label=_('principal member'),
+    ),
Are these names correct?
# ipa servicedelegationrule-find
----------------------------------
1 service delegation rule matched
----------------------------------
    Delegation name: ipa-http-delegation
    Allowed Target: ipa-ldap-delegation-targets,
ipa-cifs-delegation-targets
    Failed principals: HTTP/vm-093.example....@example.com

Fixed.



2)
+ 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',

This regex does not allow space inside
In [6]: print re.match(pattern, 'lalalala lalala')
None

Fixed. I'm tempted to just drop this regex entirely. Other plugins have no such restrictions, but this should work better now.


3)
+            yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
+                      label=_('member %s') % name,
+                      csv=True, alwaysask=True)

IMHO CSV values should not be supported.
Honza told me, the option doesn't work anyway.

Yeah, a copy and paste issue.

Patch with minor fixes attached.

I removed unused code and PEP8 complains

Incorporated and fixed a number of other things, including some typos in the doc examples.

rob


>From 8458d605dcbe30191dda561d60d0145b44a65cfb 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 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
---
 ACI.txt                                            |  16 +
 API.txt                                            | 153 ++++++
 VERSION                                            |   4 +-
 install/updates/20-indices.update                  |   9 +
 install/updates/25-referint.update                 |   1 +
 ipalib/plugins/servicedelegation.py                | 537 +++++++++++++++++++
 ipatests/test_xmlrpc/objectclasses.py              |  11 +
 .../test_xmlrpc/test_servicedelegation_plugin.py   | 591 +++++++++++++++++++++
 8 files changed, 1320 insertions(+), 2 deletions(-)
 create mode 100644 ipalib/plugins/servicedelegation.py
 create mode 100644 ipatests/test_xmlrpc/test_servicedelegation_plugin.py

diff --git a/ACI.txt b/ACI.txt
index 3c4ebde..1821696 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -212,6 +212,22 @@ dn: cn=services,cn=accounts,dc=ipa,dc=example
 aci: (targetattr = "createtimestamp || entryusn || ipakrbauthzdata || ipakrbprincipalalias || ipauniqueid || krbcanonicalname || krblastpwdchange || krbobjectreferences || krbpasswordexpiration || krbprincipalaliases || krbprincipalexpiration || krbprincipalname || managedby || memberof || modifytimestamp || objectclass || usercertificate")(targetfilter = "(objectclass=ipaservice)")(version 3.0;acl "permission:System: Read Services";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: cn=services,cn=accounts,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipaservice)")(version 3.0;acl "permission:System: Remove Services";allow (delete) groupdn = "ldap:///cn=System: Remove Services,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=s4u2proxy,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=groupofprincipals)")(version 3.0;acl "permission:System: Add Service Delegations";allow (add) groupdn = "ldap:///cn=System: Add Service Delegations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=s4u2proxy,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "ipaallowedtarget || memberprincipal")(targetfilter = "(objectclass=groupofprincipals)")(version 3.0;acl "permission:System: Modify Service Delegation Membership";allow (write) groupdn = "ldap:///cn=System: Modify Service Delegation Membership,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=s4u2proxy,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || entryusn || ipaallowedtarget || memberprincipal || modifytimestamp || objectclass")(targetfilter = "(objectclass=groupofprincipals)")(version 3.0;acl "permission:System: Read Service Delegations";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Service Delegations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=s4u2proxy,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=groupofprincipals)")(version 3.0;acl "permission:System: Remove Service Delegations";allow (delete) groupdn = "ldap:///cn=System: Remove Service Delegations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=s4u2proxy,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=groupofprincipals)")(version 3.0;acl "permission:System: Add Service Delegations";allow (add) groupdn = "ldap:///cn=System: Add Service Delegations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=s4u2proxy,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "ipaallowedtarget || memberprincipal")(targetfilter = "(objectclass=groupofprincipals)")(version 3.0;acl "permission:System: Modify Service Delegation Membership";allow (write) groupdn = "ldap:///cn=System: Modify Service Delegation Membership,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=s4u2proxy,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || entryusn || ipaallowedtarget || memberprincipal || modifytimestamp || objectclass")(targetfilter = "(objectclass=groupofprincipals)")(version 3.0;acl "permission:System: Read Service Delegations";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Service Delegations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=s4u2proxy,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=groupofprincipals)")(version 3.0;acl "permission:System: Remove Service Delegations";allow (delete) groupdn = "ldap:///cn=System: Remove Service Delegations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=staged users,cn=accounts,cn=provisioning,dc=ipa,dc=example
 aci: (targetattr = "*")(target = "ldap:///uid=*,cn=staged users,cn=accounts,cn=provisioning,dc=ipa,dc=example")(targetfilter = "(objectclass=*)")(version 3.0;acl "permission:System: Add Stage Users by Provisioning and Administrators";allow (add) groupdn = "ldap:///cn=System: Add Stage Users by Provisioning and Administrators,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=staged users,cn=accounts,cn=provisioning,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 3cfcf34..6520f2c 100644
--- a/API.txt
+++ b/API.txt
@@ -3730,6 +3730,159 @@ 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: servicedelegationrule_add
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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: 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: servicedelegationrule_add_member
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+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: servicedelegationrule_add_target
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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: Str('servicedelegationtarget*', alwaysask=True, cli_name='servicedelegationtargets', 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: servicedelegationrule_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=True, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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: servicedelegationrule_find
+args: 1,8,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='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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: 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: servicedelegationrule_remove_member
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+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: servicedelegationrule_remove_target
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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: Str('servicedelegationtarget*', alwaysask=True, cli_name='servicedelegationtargets', 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: servicedelegationrule_show
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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: servicedelegationtarget_add
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+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: servicedelegationtarget_add_member
+args: 1,4,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[a-zA-Z0-9_.-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('principal*', alwaysask=True, cli_name='principals')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+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: servicedelegationtarget_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=True, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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: servicedelegationtarget_find
+args: 1,7,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='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[a-zA-Z0-9_.-]?$', primary_key=True, query=True, required=False)
+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: 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: servicedelegationtarget_remove_member
+args: 1,4,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[a-zA-Z0-9_.-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('principal*', alwaysask=True, cli_name='principals')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+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: servicedelegationtarget_show
+args: 1,4,3
+arg: Str('cn', attribute=True, cli_name='delegation_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[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('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 24a2913..2ad3827 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=122
-# Last change: ftweedal - allow multiple host/service certificates
+IPA_API_VERSION_MINOR=123
+# Last change: rcritten - added service constraint delegation plugin
diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update
index e6e4888..880e73f 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 609eaba..005cd03 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/servicedelegation.py b/ipalib/plugins/servicedelegation.py
new file mode 100644
index 0000000..34312e3
--- /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',
+    }
diff --git a/ipatests/test_xmlrpc/objectclasses.py b/ipatests/test_xmlrpc/objectclasses.py
index 9a69cf3..a5c1b4c 100644
--- a/ipatests/test_xmlrpc/objectclasses.py
+++ b/ipatests/test_xmlrpc/objectclasses.py
@@ -201,3 +201,14 @@ idoverridegroup = [
     u'top',
     u'ipaGroupOverride',
 ]
+
+servicedelegationrule = [
+    u'top',
+    u'groupofprincipals',
+    u'ipakrb5delegationacl',
+]
+
+servicedelegationtarget = [
+    u'top',
+    u'groupofprincipals',
+]
diff --git a/ipatests/test_xmlrpc/test_servicedelegation_plugin.py b/ipatests/test_xmlrpc/test_servicedelegation_plugin.py
new file mode 100644
index 0000000..6ad441d
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_servicedelegation_plugin.py
@@ -0,0 +1,591 @@
+#
+# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+#
+"""
+Test the `ipalib/plugins/serviceconstraint.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative
+from ipapython.dn import DN
+
+rule1 = u'test1'
+rule2 = u'test rule two'
+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)
+
+
+def get_servicedelegation_dn(cn):
+    return DN(('cn', cn), api.env.container_s4u2proxy, api.env.basedn)
+
+
+class test_servicedelegation(Declarative):
+    cleanup_commands = [
+        ('servicedelegationrule_del', [rule1], {}),
+        ('servicedelegationrule_del', [rule2], {}),
+        ('servicedelegationtarget_del', [target1], {}),
+        ('servicedelegationtarget_del', [target2], {}),
+    ]
+
+    tests = [
+
+        ################
+        # create rule1:
+        dict(
+            desc='Try to retrieve non-existent %r' % rule1,
+            command=('servicedelegationrule_show', [rule1], {}),
+            expected=errors.NotFound(
+                reason=u'%s: service delegation rule not found' % rule1
+            ),
+        ),
+
+
+        dict(
+            desc='Try to delete non-existent %r' % rule1,
+            command=('servicedelegationrule_del', [rule1], {}),
+            expected=errors.NotFound(
+                reason=u'%s: service delegation rule not found' % rule1
+            ),
+        ),
+
+
+        dict(
+            desc='Create %r' % rule1,
+            command=(
+                'servicedelegationrule_add', [rule1], {}
+            ),
+            expected=dict(
+                value=rule1,
+                summary=u'Added service delegation rule "%s"' % rule1,
+                result=dict(
+                    cn=[rule1],
+                    objectclass=objectclasses.servicedelegationrule,
+                    dn=get_servicedelegation_dn(rule1),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Try to create duplicate %r' % rule1,
+            command=(
+                'servicedelegationrule_add', [rule1], {}
+            ),
+            expected=errors.DuplicateEntry(
+                message=u'service delegation rule with name "%s" '
+                'already exists' % rule1),
+        ),
+
+
+        dict(
+            desc='Retrieve %r' % rule1,
+            command=('servicedelegationrule_show', [rule1], {}),
+            expected=dict(
+                value=rule1,
+                summary=None,
+                result=dict(
+                    cn=[rule1],
+                    dn=get_servicedelegation_dn(rule1),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Search for %r' % rule1,
+            command=('servicedelegationrule_find', [], dict(cn=rule1)),
+            expected=dict(
+                count=1,
+                truncated=False,
+                result=[
+                    dict(
+                        dn=get_servicedelegation_dn(rule1),
+                        cn=[rule1],
+                    ),
+                ],
+                summary=u'1 service delegation rule matched',
+            ),
+        ),
+
+
+
+        ################
+        # create rule2:
+        dict(
+            desc='Create %r' % rule2,
+            command=(
+                'servicedelegationrule_add', [rule2], {}
+            ),
+            expected=dict(
+                value=rule2,
+                summary=u'Added service delegation rule "%s"' % rule2,
+                result=dict(
+                    cn=[rule2],
+                    objectclass=objectclasses.servicedelegationrule,
+                    dn=get_servicedelegation_dn(rule2),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Search for all rules',
+            command=('servicedelegationrule_find', [], {}),
+            expected=dict(
+                summary=u'3 service delegation rules matched',
+                count=3,
+                truncated=False,
+                result=[
+                    {
+                        'dn': get_servicedelegation_dn(u'ipa-http-delegation'),
+                        'cn': [u'ipa-http-delegation'],
+                        'memberprincipal': [princ1],
+                        'ipaallowedtarget_servicedelegationtarget':
+                            [u'ipa-ldap-delegation-targets',
+                             u'ipa-cifs-delegation-targets']
+                    },
+                    dict(
+                        dn=get_servicedelegation_dn(rule2),
+                        cn=[rule2],
+                    ),
+                    dict(
+                        dn=get_servicedelegation_dn(rule1),
+                        cn=[rule1],
+                    ),
+                ],
+            ),
+        ),
+
+
+        dict(
+            desc='Create target %r' % target1,
+            command=(
+                'servicedelegationtarget_add', [target1], {}
+            ),
+            expected=dict(
+                value=target1,
+                summary=u'Added service delegation target "%s"' % target1,
+                result=dict(
+                    cn=[target1],
+                    objectclass=objectclasses.servicedelegationtarget,
+                    dn=get_servicedelegation_dn(target1),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Create target %r' % target2,
+            command=(
+                'servicedelegationtarget_add', [target2], {}
+            ),
+            expected=dict(
+                value=target2,
+                summary=u'Added service delegation target "%s"' % target2,
+                result=dict(
+                    cn=[target2],
+                    objectclass=objectclasses.servicedelegationtarget,
+                    dn=get_servicedelegation_dn(target2),
+                ),
+            ),
+        ),
+
+
+        dict(
+            desc='Search for all targets',
+            command=('servicedelegationtarget_find', [], {}),
+            expected=dict(
+                summary=u'4 service delegation targets matched',
+                count=4,
+                truncated=False,
+                result=[
+                    {
+                        'dn': get_servicedelegation_dn(
+                            u'ipa-cifs-delegation-targets'),
+                        'cn': [u'ipa-cifs-delegation-targets'],
+                    },
+                    {
+                        'dn': get_servicedelegation_dn(
+                            u'ipa-ldap-delegation-targets'
+                        ),
+                        'cn': [u'ipa-ldap-delegation-targets'],
+                        'memberprincipal': [princ2],
+                    },
+                    dict(
+                        dn=get_servicedelegation_dn(target1),
+                        cn=[target1],
+                    ),
+                    dict(
+                        dn=get_servicedelegation_dn(target2),
+                        cn=[target2],
+                    ),
+                ],
+            ),
+        ),
+
+
+        ###############
+        # member stuff:
+        dict(
+            desc='Add member %r to %r' % (target1, rule1),
+            command=(
+                'servicedelegationrule_add_target', [rule1],
+                dict(servicedelegationtarget=target1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        servicedelegationtarget=tuple(),
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'ipaallowedtarget_servicedelegationtarget': (target1,),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add duplicate target %r to %r' % (target1, rule1),
+            command=(
+                'servicedelegationrule_add_target', [rule1],
+                dict(servicedelegationtarget=target1)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        servicedelegationtarget=[
+                            [target1, u'This entry is already a member']
+                        ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'ipaallowedtarget_servicedelegationtarget': (target1,),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add non-existent target %r to %r' % (u'notfound', rule1),
+            command=(
+                'servicedelegationrule_add_target', [rule1],
+                dict(servicedelegationtarget=u'notfound')
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        servicedelegationtarget=[
+                            [u'notfound', u'no such entry']
+                        ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'ipaallowedtarget_servicedelegationtarget': (target1,),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove a target %r from %r' % (target1, rule1),
+            command=(
+                'servicedelegationrule_remove_target', [rule1],
+                dict(servicedelegationtarget=target1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        servicedelegationtarget=tuple(),
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove non-existent target %r from %r' % (
+                u'notfound', rule1
+            ),
+            command=(
+                'servicedelegationrule_remove_target', [rule1],
+                dict(servicedelegationtarget=u'notfound')
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    ipaallowedtarget=dict(
+                        servicedelegationtarget=[
+                            [u'notfound', u'This entry is not a member']
+                        ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        ###############
+        # memberprincipal member stuff:
+        dict(
+            desc='Add memberprinc %r to %r' % (princ1, rule1),
+            command=(
+                'servicedelegationrule_add_member', [rule1],
+                dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=tuple(),
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'memberprincipal': (princ1,),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add duplicate member %r to %r' % (princ1, rule1),
+            command=(
+                'servicedelegationrule_add_member', [rule1],
+                dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=[
+                            [princ1, u'This entry is already a member']
+                        ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'memberprincipal': (princ1,),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add non-existent member %r to %r' % (
+                u'HTTP/notfound', rule1
+            ),
+            command=(
+                'servicedelegationrule_add_member', [rule1],
+                dict(principal=u'HTTP/notfound@%s' % api.env.realm)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=[
+                            [u'HTTP/notfound@%s' % api.env.realm,
+                             u'no such entry']
+                            ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'memberprincipal': (princ1,),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove a member %r from %r' % (princ1, rule1),
+            command=(
+                'servicedelegationrule_remove_member', [rule1],
+                dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=tuple(),
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'memberprincipal': [],
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove non-existent member %r from %r' % (
+                 u'HTTP/notfound', rule1
+            ),
+            command=(
+                'servicedelegationrule_remove_member', [rule1],
+                dict(principal=u'HTTP/notfound@%s' % api.env.realm)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=[
+                            [u'HTTP/notfound@%s' % api.env.realm,
+                             u'This entry is not a member']
+                        ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(rule1),
+                    'cn': [rule1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add memberprinc %r to %r' % (princ1, target1),
+            command=(
+                'servicedelegationtarget_add_member', [target1],
+                dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=tuple(),
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(target1),
+                    'memberprincipal': (princ1,),
+                    'cn': [target1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add duplicate member %r to %r' % (princ1, target1),
+            command=(
+                'servicedelegationtarget_add_member', [target1],
+                dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=[
+                            [princ1, u'This entry is already a member']
+                        ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(target1),
+                    'memberprincipal': (princ1,),
+                    'cn': [target1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Add non-existent member %r to %r' % (
+                u'HTTP/notfound', target1
+            ),
+            command=(
+                'servicedelegationtarget_add_member', [target1],
+                dict(principal=u'HTTP/notfound@%s' % api.env.realm)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=[
+                            [u'HTTP/notfound@%s' % api.env.realm,
+                             u'no such entry']
+                        ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(target1),
+                    'memberprincipal': (princ1,),
+                    'cn': [target1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove a member %r from %r' % (princ1, target1),
+            command=(
+                'servicedelegationtarget_remove_member', [target1],
+                dict(principal=princ1)
+            ),
+            expected=dict(
+                completed=1,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=tuple(),
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(target1),
+                    'memberprincipal': [],
+                    'cn': [target1],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Remove non-existent member %r from %r' % (
+                u'HTTP/notfound', target1
+            ),
+            command=(
+                'servicedelegationtarget_remove_member', [target1],
+                dict(principal=u'HTTP/notfound@%s' % api.env.realm)
+            ),
+            expected=dict(
+                completed=0,
+                failed=dict(
+                    failed_memberprincipal=dict(
+                        memberprincipal=[
+                            [u'HTTP/notfound@%s' % api.env.realm,
+                             u'This entry is not a member']
+                        ],
+                    ),
+                ),
+                result={
+                    'dn': get_servicedelegation_dn(target1),
+                    'cn': [target1],
+                },
+            ),
+        ),
+
+    ]
-- 
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