diff options
-rw-r--r-- | ACI.txt | 16 | ||||
-rw-r--r-- | API.txt | 153 | ||||
-rw-r--r-- | VERSION | 4 | ||||
-rw-r--r-- | install/updates/20-indices.update | 9 | ||||
-rw-r--r-- | install/updates/25-referint.update | 1 | ||||
-rw-r--r-- | ipalib/plugins/servicedelegation.py | 537 | ||||
-rw-r--r-- | ipatests/test_xmlrpc/objectclasses.py | 11 | ||||
-rw-r--r-- | ipatests/test_xmlrpc/test_servicedelegation_plugin.py | 591 |
8 files changed, 1320 insertions, 2 deletions
@@ -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 @@ -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') @@ -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], + }, + ), + ), + + ] |