summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ACI.txt16
-rw-r--r--API.txt153
-rw-r--r--VERSION4
-rw-r--r--install/updates/20-indices.update9
-rw-r--r--install/updates/25-referint.update1
-rw-r--r--ipalib/plugins/servicedelegation.py537
-rw-r--r--ipatests/test_xmlrpc/objectclasses.py11
-rw-r--r--ipatests/test_xmlrpc/test_servicedelegation_plugin.py591
8 files changed, 1320 insertions, 2 deletions
diff --git a/ACI.txt b/ACI.txt
index 3c4ebde5b..1821696fd 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 3cfcf3493..6520f2c42 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 24a291322..2ad382792 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 e6e4888e2..880e73f3b 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 609eaba74..005cd0376 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 000000000..34312e39a
--- /dev/null
+++ b/ipalib/plugins/servicedelegation.py
@@ -0,0 +1,537 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+from ipalib import api
+from ipalib import Str
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import *
+from ipalib.plugins.service import normalize_principal
+from ipalib import _, ngettext
+
+__doc__ = _("""
+Service Constrained Delegation
+
+Manage rules to allow constrained delegation of credentials so
+that a service can impersonate a user when communicating with another
+service without requiring the user to actually forward their TGT.
+This makes for a much better method of delegating credentials as it
+prevents exposure of the short term secret of the user.
+
+The naming convention is to append the word "target" or "targets" to
+a matching rule name. This is not mandatory but helps conceptually
+to associate rules and targets.
+
+A rule consists of two things:
+ - A list of targets the rule applies to
+ - A list of memberPrincipals that are allowed to delegate for
+ those targets
+
+A target consists of a list of principals that can be delegated.
+
+In English, a rule says that this principal can delegate as this
+list of principals, as defined by these targets.
+
+EXAMPLES:
+
+ Add a new constrained delegation rule:
+ ipa servicedelegationrule-add ftp-delegation
+
+ Add a new constrained delegation target:
+ ipa servicedelegationtarget-add ftp-delegation-target
+
+ Add a principal to the rule:
+ ipa servicedelegationrule-add-member --principals=ftp/ipa.example.com \
+ ftp-delegation
+
+ Add our target to the rule:
+ ipa servicedelegationrule-add-target \
+ --servicedelegationtargets=ftp-delegation-target ftp-delegation
+
+ Add a principal to the target:
+ ipa servicedelegationtarget-add-member --principals=ldap/ipa.example.com \
+ ftp-delegation-target
+
+ Display information about a named delegation rule and target:
+ ipa servicedelegationrule_show ftp-delegation
+ ipa servicedelegationtarget_show ftp-delegation-target
+
+ Remove a constrained delegation:
+ ipa servicedelegationrule-del ftp-delegation-target
+ ipa servicedelegationtarget-del ftp-delegation
+
+In this example the ftp service can get a TGT for the ldap service on
+the bound user's behalf.
+
+It is strongly discouraged to modify the delegations that ship with
+IPA, ipa-http-delegation and its targets ipa-cifs-delegation-targets and
+ipa-ldap-delegation-targets. Incorrect changes can remove the ablity
+to delegate, causing the framework to stop functioning.
+""")
+
+register = Registry()
+
+PROTECTED_CONSTRAINT_RULES = (
+ u'ipa-http-delegation',
+)
+
+PROTECTED_CONSTRAINT_TARGETS = (
+ u'ipa-cifs-delegation-targets',
+ u'ipa-ldap-delegation-targets',
+
+)
+
+
+output_params = (
+ Str(
+ 'ipaallowedtarget_servicedelegationtarget',
+ label=_('Allowed Target'),
+ ),
+ Str(
+ 'ipaallowedtoimpersonate',
+ label=_('Allowed to Impersonate'),
+ ),
+ Str(
+ 'memberprincipal',
+ label=_('Member principals'),
+ ),
+ Str(
+ 'failed_memberprincipal',
+ label=_('Failed members'),
+ ),
+ Str(
+ 'ipaallowedtarget',
+ label=_('Failed targets'),
+ ),
+)
+
+
+class servicedelegation(LDAPObject):
+ """
+ Service Constrained Delegation base object.
+
+ This jams a couple of concepts into a single plugin because the
+ data is all stored in one place. There is a "rule" which has the
+ objectclass ipakrb5delegationacl. This is the entry that controls
+ the delegation. Other entries that lack this objectclass are
+ targets and define what services can be impersonated.
+ """
+ container_dn = api.env.container_s4u2proxy
+ object_class = ['groupofprincipals', 'top']
+
+ managed_permissions = {
+ 'System: Read Service Delegations': {
+ 'ipapermbindruletype': 'permission',
+ 'ipapermright': {'read', 'search', 'compare'},
+ 'ipapermtargetfilter': {'(objectclass=groupofprincipals)'},
+ 'ipapermdefaultattr': {
+ 'cn', 'objectclass', 'memberprincipal',
+ 'ipaallowedtarget',
+ },
+ 'default_privileges': {'Service Administrators'},
+ },
+ 'System: Add Service Delegations': {
+ 'ipapermright': {'add'},
+ 'ipapermtargetfilter': {'(objectclass=groupofprincipals)'},
+ 'default_privileges': {'Service Administrators'},
+ },
+ 'System: Remove Service Delegations': {
+ 'ipapermright': {'delete'},
+ 'ipapermtargetfilter': {'(objectclass=groupofprincipals)'},
+ 'default_privileges': {'Service Administrators'},
+ },
+ 'System: Modify Service Delegation Membership': {
+ 'ipapermright': {'write'},
+ 'ipapermtargetfilter': {'(objectclass=groupofprincipals)'},
+ 'ipapermdefaultattr': {'memberprincipal', 'ipaallowedtarget'},
+ 'default_privileges': {'Service Administrators'},
+ },
+ }
+
+ rdn_is_primary_key = True
+
+ takes_params = (
+ Str(
+ 'cn',
+ pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[a-zA-Z0-9_.-]?$',
+ pattern_errmsg='may only include letters, numbers, _, -, ., '
+ 'and a space inside',
+ maxlength=255,
+ cli_name='delegation_name',
+ label=_('Delegation name'),
+ primary_key=True,
+ ),
+ )
+
+
+class servicedelegation_add_member(LDAPAddMember):
+ __doc__ = _('Add target to a named service delegation.')
+ member_attrs = ['memberprincipal']
+ member_attributes = []
+ member_names = {}
+ principal_attr = 'memberprincipal'
+ principal_failedattr = 'failed_memberprincipal'
+
+ has_output_params = LDAPAddMember.has_output_params + output_params
+
+ def get_options(self):
+ for option in super(servicedelegation_add_member, self).get_options():
+ yield option
+ for attr in self.member_attrs:
+ name = self.member_names[attr]
+ doc = self.member_param_doc % name
+ yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
+ label=_('member %s') % name, alwaysask=True)
+
+ def get_member_dns(self, **options):
+ """
+ There are no member_dns to return. memberPrincipal needs
+ special handling since it is just a principal, not a
+ full dn.
+ """
+ return dict(), dict()
+
+ def post_callback(self, ldap, completed, failed, dn, entry_attrs,
+ *keys, **options):
+ """
+ Add memberPrincipal values. This is done afterward because it isn't
+ a DN and the LDAPAddMember method explicitly only handles DNs.
+
+ A separate fake attribute name is used for failed members. This is
+ a reverse of the way this is typically handled in the *Member
+ routines, where a successful addition will be represented as
+ member/memberof_<attribute>. In this case, because memberPrincipal
+ isn't a DN, I'm doing the reverse, and creating a fake failed
+ attribute instead.
+ """
+ ldap = self.obj.backend
+ members = []
+ failed[self.principal_failedattr] = {}
+ failed[self.principal_failedattr][self.principal_attr] = []
+ names = options.get(self.member_names[self.principal_attr], [])
+ ldap_obj = self.api.Object['service']
+ if names:
+ for name in names:
+ if not name:
+ continue
+ name = normalize_principal(name)
+ obj_dn = ldap_obj.get_dn(name)
+ try:
+ ldap.get_entry(obj_dn, ['krbprincipalname'])
+ except errors.NotFound as e:
+ failed[self.principal_failedattr][
+ self.principal_attr].append((name, unicode(e)))
+ continue
+ try:
+ if name not in entry_attrs.get(self.principal_attr, []):
+ members.append(name)
+ else:
+ raise errors.AlreadyGroupMember()
+ except errors.PublicError as e:
+ failed[self.principal_failedattr][
+ self.principal_attr].append((name, unicode(e)))
+ else:
+ completed += 1
+
+ if members:
+ value = entry_attrs.setdefault(self.principal_attr, [])
+ value.extend(members)
+
+ try:
+ ldap.update_entry(entry_attrs)
+ except errors.EmptyModlist:
+ pass
+
+ return completed, dn
+
+
+class servicedelegation_remove_member(LDAPRemoveMember):
+ __doc__ = _('Remove member from a named service delegation.')
+
+ member_attrs = ['memberprincipal']
+ member_attributes = []
+ member_names = {}
+ principal_attr = 'memberprincipal'
+ principal_failedattr = 'failed_memberprincipal'
+
+ has_output_params = LDAPRemoveMember.has_output_params + output_params
+
+ def get_options(self):
+ for option in super(
+ servicedelegation_remove_member, self).get_options():
+ yield option
+ for attr in self.member_attrs:
+ name = self.member_names[attr]
+ doc = self.member_param_doc % name
+ yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
+ label=_('member %s') % name, alwaysask=True)
+
+ def get_member_dns(self, **options):
+ """
+ Need to ignore memberPrincipal for now and handle the difference
+ in objectclass between a rule and a target.
+ """
+ dns = {}
+ failed = {}
+ for attr in self.member_attrs:
+ dns[attr] = {}
+ if attr.lower() == 'memberprincipal':
+ # This will be handled later. memberprincipal isn't a
+ # DN so will blow up in assertions in baseldap.
+ continue
+ failed[attr] = {}
+ for ldap_obj_name in self.obj.attribute_members[attr]:
+ dns[attr][ldap_obj_name] = []
+ failed[attr][ldap_obj_name] = []
+ names = options.get(self.member_names[attr], [])
+ if not names:
+ continue
+ for name in names:
+ if not name:
+ continue
+ ldap_obj = self.api.Object[ldap_obj_name]
+ try:
+ dns[attr][ldap_obj_name].append(ldap_obj.get_dn(name))
+ except errors.PublicError as e:
+ failed[attr][ldap_obj_name].append((name, unicode(e)))
+ return dns, failed
+
+ def post_callback(self, ldap, completed, failed, dn, entry_attrs,
+ *keys, **options):
+ """
+ Remove memberPrincipal values. This is done afterward because it
+ isn't a DN and the LDAPAddMember method explicitly only handles DNs.
+
+ See servicedelegation_add_member() for an explanation of what
+ failedattr is.
+ """
+ ldap = self.obj.backend
+ failed[self.principal_failedattr] = {}
+ failed[self.principal_failedattr][self.principal_attr] = []
+ names = options.get(self.member_names[self.principal_attr], [])
+ if names:
+ for name in names:
+ if not name:
+ continue
+ name = normalize_principal(name)
+ try:
+ if name in entry_attrs.get(self.principal_attr, []):
+ entry_attrs[self.principal_attr].remove(name)
+ else:
+ raise errors.NotGroupMember()
+ except errors.PublicError as e:
+ failed[self.principal_failedattr][
+ self.principal_attr].append((name, unicode(e)))
+ else:
+ completed += 1
+
+ try:
+ ldap.update_entry(entry_attrs)
+ except errors.EmptyModlist:
+ pass
+
+ return completed, dn
+
+
+@register()
+class servicedelegationrule(servicedelegation):
+ """
+ A service delegation rule. This is the ACL that controls
+ what can be delegated to whom.
+ """
+ object_name = _('service delegation rule')
+ object_name_plural = _('service delegation rules')
+ object_class = ['ipakrb5delegationacl', 'groupofprincipals', 'top']
+ default_attributes = [
+ 'cn', 'memberprincipal', 'ipaallowedtarget',
+ 'ipaallowedtoimpersonate',
+ ]
+ attribute_members = {
+ # memberprincipal is not listed because it isn't a DN
+ 'ipaallowedtarget': ['servicedelegationtarget'],
+ }
+
+ label = _('Service delegation rules')
+ label_singular = _('Service delegation rule')
+
+
+@register()
+class servicedelegationrule_add(LDAPCreate):
+ __doc__ = _('Create a new service delegation rule.')
+
+ msg_summary = _('Added service delegation rule "%(value)s"')
+
+
+@register()
+class servicedelegationrule_del(LDAPDelete):
+ __doc__ = _('Delete service delegation.')
+
+ msg_summary = _('Deleted service delegation "%(value)s"')
+
+ def pre_callback(self, ldap, dn, *keys, **options):
+ assert isinstance(dn, DN)
+ if keys[0] in PROTECTED_CONSTRAINT_RULES:
+ raise errors.ProtectedEntryError(
+ label=_(u'service delegation rule'),
+ key=keys[0],
+ reason=_(u'privileged service delegation rule')
+ )
+ return dn
+
+
+@register()
+class servicedelegationrule_find(LDAPSearch):
+ __doc__ = _('Search for service delegations rule.')
+
+ has_output_params = LDAPSearch.has_output_params + output_params
+
+ msg_summary = ngettext(
+ '%(count)d service delegation rule matched',
+ '%(count)d service delegation rules matched', 0
+ )
+
+
+@register()
+class servicedelegationrule_show(LDAPRetrieve):
+ __doc__ = _('Display information about a named service delegation rule.')
+
+ has_output_params = LDAPRetrieve.has_output_params + output_params
+
+
+@register()
+class servicedelegationrule_add_member(servicedelegation_add_member):
+ __doc__ = _('Add member to a named service delegation rule.')
+
+ member_names = {
+ 'memberprincipal': 'principal',
+ }
+
+
+@register()
+class servicedelegationrule_remove_member(servicedelegation_remove_member):
+ __doc__ = _('Remove member from a named service delegation rule.')
+ member_names = {
+ 'memberprincipal': 'principal',
+ }
+
+
+@register()
+class servicedelegationrule_add_target(LDAPAddMember):
+ __doc__ = _('Add target to a named service delegation rule.')
+
+ member_attributes = ['ipaallowedtarget']
+ attribute_members = {
+ 'ipaallowedtarget': ['servicedelegationtarget'],
+ }
+ has_output_params = LDAPAddMember.has_output_params + output_params
+
+
+@register()
+class servicedelegationrule_remove_target(LDAPRemoveMember):
+ __doc__ = _('Remove target from a named service delegation rule.')
+ member_attributes = ['ipaallowedtarget']
+ attribute_members = {
+ 'ipaallowedtarget': ['servicedelegationtarget'],
+ }
+ has_output_params = LDAPRemoveMember.has_output_params + output_params
+
+
+@register()
+class servicedelegationtarget(servicedelegation):
+ object_name = _('service delegation target')
+ object_name_plural = _('service delegation targets')
+ object_class = ['groupofprincipals', 'top']
+ default_attributes = [
+ 'cn', 'memberprincipal',
+ ]
+ attribute_members = {}
+
+ label = _('Service delegation targets')
+ label_singular = _('Service delegation target')
+
+
+@register()
+class servicedelegationtarget_add(LDAPCreate):
+ __doc__ = _('Create a new service delegation target.')
+
+ msg_summary = _('Added service delegation target "%(value)s"')
+
+
+@register()
+class servicedelegationtarget_del(LDAPDelete):
+ __doc__ = _('Delete service delegation target.')
+
+ msg_summary = _('Deleted service delegation target "%(value)s"')
+
+ def pre_callback(self, ldap, dn, *keys, **options):
+ assert isinstance(dn, DN)
+ if keys[0] in PROTECTED_CONSTRAINT_TARGETS:
+ raise errors.ProtectedEntryError(
+ label=_(u'service delegation target'),
+ key=keys[0],
+ reason=_(u'privileged service delegation target')
+ )
+ return dn
+
+
+@register()
+class servicedelegationtarget_find(LDAPSearch):
+ __doc__ = _('Search for service delegation target.')
+
+ has_output_params = LDAPSearch.has_output_params + output_params
+
+ msg_summary = ngettext(
+ '%(count)d service delegation target matched',
+ '%(count)d service delegation targets matched', 0
+ )
+
+ def pre_callback(self, ldap, filters, attrs_list, base_dn, scope,
+ *args, **options):
+ """
+ Exclude rules from the search output. A target contains a subset
+ of a rule objectclass.
+ """
+ search_kw = self.args_options_2_entry(**options)
+ search_kw['objectclass'] = self.obj.object_class
+ attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL)
+ rule_kw = {'objectclass': 'ipakrb5delegationacl'}
+ target_filter = ldap.make_filter(rule_kw, rules=ldap.MATCH_NONE)
+ attr_filter = ldap.combine_filters(
+ (target_filter, attr_filter), rules=ldap.MATCH_ALL
+ )
+
+ search_kw = {}
+ term = args[-1]
+ for a in self.obj.default_attributes:
+ search_kw[a] = term
+
+ term_filter = ldap.make_filter(search_kw, exact=False)
+
+ sfilter = ldap.combine_filters(
+ (term_filter, attr_filter), rules=ldap.MATCH_ALL
+ )
+ return sfilter, base_dn, ldap.SCOPE_ONELEVEL
+
+
+@register()
+class servicedelegationtarget_show(LDAPRetrieve):
+ __doc__ = _('Display information about a named service delegation target.')
+
+ has_output_params = LDAPRetrieve.has_output_params + output_params
+
+
+@register()
+class servicedelegationtarget_add_member(servicedelegation_add_member):
+ __doc__ = _('Add member to a named service delegation target.')
+
+ member_names = {
+ 'memberprincipal': 'principal',
+ }
+
+
+@register()
+class servicedelegationtarget_remove_member(servicedelegation_remove_member):
+ __doc__ = _('Remove member from a named service delegation target.')
+ member_names = {
+ 'memberprincipal': 'principal',
+ }
diff --git a/ipatests/test_xmlrpc/objectclasses.py b/ipatests/test_xmlrpc/objectclasses.py
index 9a69cf3fd..a5c1b4c50 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 000000000..6ad441d16
--- /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],
+ },
+ ),
+ ),
+
+ ]