URL: https://github.com/freeipa/freeipa/pull/399
Author: dkupka
 Title: #399: Certificate mapping test
Action: synchronized

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/399/head:pr399
git checkout pr399
From b758cf15199a42a707c1028a29ec4772d24589eb Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <f...@redhat.com>
Date: Tue, 20 Dec 2016 16:21:58 +0100
Subject: [PATCH 1/4] Support for Certificate Identity Mapping

See design http://www.freeipa.org/page/V4/Certificate_Identity_Mapping

https://fedorahosted.org/freeipa/ticket/6542
---
 ACI.txt                           |  16 +-
 API.txt                           | 154 +++++++++++++++++
 VERSION.m4                        |   4 +-
 install/share/73certmap.ldif      |  17 ++
 install/share/Makefile.am         |   1 +
 install/updates/73-certmap.update |  27 +++
 install/updates/Makefile.am       |   1 +
 ipalib/constants.py               |   2 +
 ipaserver/install/dsinstance.py   |   1 +
 ipaserver/plugins/baseuser.py     |  11 +-
 ipaserver/plugins/certmap.py      | 345 ++++++++++++++++++++++++++++++++++++++
 ipaserver/plugins/user.py         | 173 ++++++++++++++++++-
 12 files changed, 745 insertions(+), 7 deletions(-)
 create mode 100644 install/share/73certmap.ldif
 create mode 100644 install/updates/73-certmap.update
 create mode 100644 ipaserver/plugins/certmap.py

diff --git a/ACI.txt b/ACI.txt
index 0b47489..ec2eeca 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -40,6 +40,18 @@ dn: cn=caacls,cn=ca,dc=ipa,dc=example
 aci: (targetattr = "cn || description || ipaenabledflag")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Modify CA ACL";allow (write) groupdn = "ldap:///cn=System: Modify CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=caacls,cn=ca,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || description || entryusn || hostcategory || ipacacategory || ipacertprofilecategory || ipaenabledflag || ipamemberca || ipamembercertprofile || ipauniqueid || member || memberhost || memberservice || memberuser || modifytimestamp || objectclass || servicecategory || usercategory")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Read CA ACLs";allow (compare,read,search) userdn = "ldap:///all";;)
+dn: cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "ipacertmappromptusername")(targetfilter = "(objectclass=ipacertmapconfigobject)")(version 3.0;acl "permission:System: Modify Certmap Configuration";allow (write) groupdn = "ldap:///cn=System: Modify Certmap Configuration,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "cn || ipacertmappromptusername")(targetfilter = "(objectclass=ipacertmapconfigobject)")(version 3.0;acl "permission:System: Read Certmap Configuration";allow (compare,read,search) userdn = "ldap:///all";;)
+dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Add Certmap Rules";allow (add) groupdn = "ldap:///cn=System: Add Certmap Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Delete Certmap Rules";allow (delete) groupdn = "ldap:///cn=System: Delete Certmap Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "associateddomain || cn || description || ipacertmapissuer || ipacertmapmaprule || ipacertmapmatchrule || ipacertmappriority || ipaenabledflag || objectclass")(targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Modify Certmap Rules";allow (write) groupdn = "ldap:///cn=System: Modify Certmap Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "associateddomain || cn || createtimestamp || description || entryusn || ipacertmapissuer || ipacertmapmaprule || ipacertmapmatchrule || ipacertmappriority || ipaenabledflag || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Read Certmap Rules";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Certmap Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
@@ -337,6 +349,8 @@ aci: (targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:S
 dn: cn=users,cn=accounts,dc=ipa,dc=example
 aci: (targetattr = "krbprincipalkey || passwordhistory || sambalmpassword || sambantpassword || userpassword")(targetfilter = "(&(!(memberOf=cn=admins,cn=groups,cn=accounts,dc=ipa,dc=example))(objectclass=posixaccount))")(version 3.0;acl "permission:System: Change User password";allow (write) groupdn = "ldap:///cn=System: Change User password,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=users,cn=accounts,dc=ipa,dc=example
+aci: (targetattr = "ipacertmapdata || objectclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Manage User Certificate Mappings";allow (write) groupdn = "ldap:///cn=System: Manage User Certificate Mappings,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=users,cn=accounts,dc=ipa,dc=example
 aci: (targetattr = "usercertificate")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Manage User Certificates";allow (write) groupdn = "ldap:///cn=System: Manage User Certificates,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=users,cn=accounts,dc=ipa,dc=example
 aci: (targetattr = "krbcanonicalname || krbprincipalname")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Manage User Principals";allow (write) groupdn = "ldap:///cn=System: Manage User Principals,cn=permissions,cn=pbac,dc=ipa,dc=example";)
@@ -347,7 +361,7 @@ aci: (targetattr = "businesscategory || carlicense || cn || departmentnumber ||
 dn: cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc,dc=ipa,dc=example
 aci: (targetattr = "*")(target = "ldap:///cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read UPG Definition";allow (compare,read,search) groupdn = "ldap:///cn=System: Read UPG Definition,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber || destinationindicator || employeenumber || employeetype || facsimiletelephonenumber || homephone || homepostaladdress || inetuserhttpurl || inetuserstatus || internationalisdnnumber || jpegphoto || l || labeleduri || mail || mobile || o || ou || pager || photo || physicaldeliveryofficename || postaladdress || postalcode || postofficebox || preferreddeliverymethod || preferredlanguage || registeredaddress || roomnumber || secretary || seealso || st || street || telephonenumber || teletexterminalidentifier || telexnumber || usercertificate || usersmimecertificate || x121address || x500uniqueidentifier")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Addressbook Attributes";allow (compare,read,search) userdn = "ldap:///all";;)
+aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber || destinationindicator || employeenumber || employeetype || facsimiletelephonenumber || homephone || homepostaladdress || inetuserhttpurl || inetuserstatus || internationalisdnnumber || ipacertmapdata || jpegphoto || l || labeleduri || mail || mobile || o || ou || pager || photo || physicaldeliveryofficename || postaladdress || postalcode || postofficebox || preferreddeliverymethod || preferredlanguage || registeredaddress || roomnumber || secretary || seealso || st || street || telephonenumber || teletexterminalidentifier || telexnumber || usercertificate || usersmimecertificate || x121address || x500uniqueidentifier")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Addressbook Attributes";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || entryusn || gecos || gidnumber || homedirectory || loginshell || modifytimestamp || objectclass || uid || uidnumber")(target = "ldap:///cn=users,cn=compat,dc=ipa,dc=example";)(version 3.0;acl "permission:System: Read User Compat Tree";allow (compare,read,search) userdn = "ldap:///anyone";;)
 dn: cn=users,cn=accounts,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 543cec5..ea88423 100644
--- a/API.txt
+++ b/API.txt
@@ -824,6 +824,119 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: certmapconfig_mod/1
+args: 0,8,3
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('delattr*', cli_name='delattr')
+option: Bool('ipacertmappromptusername?', autofill=False, cli_name='promptusername')
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: certmapconfig_show/1
+args: 0,4,3
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: certmaprule_add/1
+args: 1,12,3
+arg: Str('cn', cli_name='rulename')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('associateddomain*', cli_name='domain')
+option: Str('description?', cli_name='desc')
+option: DNParam('ipacertmapissuer?', cli_name='issuer')
+option: Str('ipacertmapmaprule?', cli_name='maprule')
+option: Str('ipacertmapmatchrule?', cli_name='matchrule')
+option: Int('ipacertmappriority?', cli_name='priority')
+option: Flag('ipaenabledflag?', autofill=True, default=True)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: certmaprule_del/1
+args: 1,2,3
+arg: Str('cn+', cli_name='rulename')
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?')
+output: Output('result', type=[<type 'dict'>])
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: ListOfPrimaryKeys('value')
+command: certmaprule_disable/1
+args: 1,1,3
+arg: Str('cn', cli_name='rulename')
+option: Str('version?')
+output: Output('result', type=[<type 'bool'>])
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: certmaprule_enable/1
+args: 1,1,3
+arg: Str('cn', cli_name='rulename')
+option: Str('version?')
+output: Output('result', type=[<type 'bool'>])
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: certmaprule_find/1
+args: 1,14,4
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('associateddomain*', autofill=False, cli_name='domain')
+option: Str('cn?', autofill=False, cli_name='rulename')
+option: Str('description?', autofill=False, cli_name='desc')
+option: DNParam('ipacertmapissuer?', autofill=False, cli_name='issuer')
+option: Str('ipacertmapmaprule?', autofill=False, cli_name='maprule')
+option: Str('ipacertmapmatchrule?', autofill=False, cli_name='matchrule')
+option: Int('ipacertmappriority?', autofill=False, cli_name='priority')
+option: Bool('ipaenabledflag?', autofill=False, default=True)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Int('sizelimit?', autofill=False)
+option: Int('timelimit?', autofill=False)
+option: Str('version?')
+output: Output('count', type=[<type 'int'>])
+output: ListOfEntries('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: Output('truncated', type=[<type 'bool'>])
+command: certmaprule_mod/1
+args: 1,14,3
+arg: Str('cn', cli_name='rulename')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('associateddomain*', autofill=False, cli_name='domain')
+option: Str('delattr*', cli_name='delattr')
+option: Str('description?', autofill=False, cli_name='desc')
+option: DNParam('ipacertmapissuer?', autofill=False, cli_name='issuer')
+option: Str('ipacertmapmaprule?', autofill=False, cli_name='maprule')
+option: Str('ipacertmapmatchrule?', autofill=False, cli_name='matchrule')
+option: Int('ipacertmappriority?', autofill=False, cli_name='priority')
+option: Flag('ipaenabledflag?', autofill=True, default=True)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: certmaprule_show/1
+args: 1,4,3
+arg: Str('cn', cli_name='rulename')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: certprofile_del/1
 args: 1,2,3
 arg: Str('cn+', cli_name='id')
@@ -5752,6 +5865,20 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: user_add_certmap/1
+args: 1,8,3
+arg: Str('uid', cli_name='login')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('ipacertmapdata*', alwaysask=False, cli_name='data')
+option: DNParam('issuer?', cli_name='issuer')
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: DNParam('subject?', cli_name='subject')
+option: Bytes('usercertificate*', cli_name='certificate')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: user_add_manager/1
 args: 1,5,3
 arg: Str('uid', cli_name='login')
@@ -5924,6 +6051,20 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: user_remove_certmap/1
+args: 1,8,3
+arg: Str('uid', cli_name='login')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('ipacertmapdata*', alwaysask=False, cli_name='data')
+option: DNParam('issuer?', cli_name='issuer')
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: DNParam('subject?', cli_name='subject')
+option: Bytes('usercertificate*', cli_name='certificate')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: user_remove_manager/1
 args: 1,5,3
 arg: Str('uid', cli_name='login')
@@ -6307,6 +6448,17 @@ default: cert_request/1
 default: cert_revoke/1
 default: cert_show/1
 default: cert_status/1
+default: certmapconfig/1
+default: certmapconfig_mod/1
+default: certmapconfig_show/1
+default: certmaprule/1
+default: certmaprule_add/1
+default: certmaprule_del/1
+default: certmaprule_disable/1
+default: certmaprule_enable/1
+default: certmaprule_find/1
+default: certmaprule_mod/1
+default: certmaprule_show/1
 default: certprofile/1
 default: certprofile_del/1
 default: certprofile_find/1
@@ -6741,6 +6893,7 @@ default: trustdomain_mod/1
 default: user/1
 default: user_add/1
 default: user_add_cert/1
+default: user_add_certmap/1
 default: user_add_manager/1
 default: user_add_principal/1
 default: user_del/1
@@ -6749,6 +6902,7 @@ default: user_enable/1
 default: user_find/1
 default: user_mod/1
 default: user_remove_cert/1
+default: user_remove_certmap/1
 default: user_remove_manager/1
 default: user_remove_principal/1
 default: user_show/1
diff --git a/VERSION.m4 b/VERSION.m4
index 36929ee..187092c 100644
--- a/VERSION.m4
+++ b/VERSION.m4
@@ -73,8 +73,8 @@ define(IPA_DATA_VERSION, 20100614120000)
 #                                                      #
 ########################################################
 define(IPA_API_VERSION_MAJOR, 2)
-define(IPA_API_VERSION_MINOR, 217)
-# Last change: Add options to write lightweight CA cert or chain to file
+define(IPA_API_VERSION_MINOR, 218)
+# Last change: Support for Certificate Identity Mapping
 
 
 ########################################################
diff --git a/install/share/73certmap.ldif b/install/share/73certmap.ldif
new file mode 100644
index 0000000..fb70f88
--- /dev/null
+++ b/install/share/73certmap.ldif
@@ -0,0 +1,17 @@
+## IPA Base OID:
+##
+## Attributes:          2.16.840.1.113730.3.8.22.x
+## ObjectClasses:       2.16.840.1.113730.3.8.23.y
+##
+dn: cn=schema
+attributeTypes: (2.16.840.1.113730.3.8.22.1 NAME 'ipaCertMapVersion' DESC 'IPA Certificate Mapping version' EQUALITY integerMatch ORDERING integerOrderingMatch SINGLE-VALUE SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 X-ORIGIN 'IPA v4.5' )
+attributeTypes: (2.16.840.1.113730.3.8.22.2 NAME 'ipaCertMapPromptUsername' DESC 'Prompt for the username when multiple identities are mapped to a certificate' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.5' )
+attributeTypes: (2.16.840.1.113730.3.8.22.3 NAME 'ipaCertMapMapRule' DESC 'Certificate Mapping Rule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.5' )
+attributeTypes: (2.16.840.1.113730.3.8.22.4 NAME 'ipaCertMapMatchRule' DESC 'Certificate Matching Rule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.5' )
+attributeTypes: (2.16.840.1.113730.3.8.22.5 NAME 'ipaCertMapData' DESC 'Certificate Mapping Data' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.5' )
+attributeTypes: (2.16.840.1.113730.3.8.22.6 NAME 'ipaCertMapIssuer' DESC 'Certificate Issuer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v4.5' )
+attributeTypes: (2.16.840.1.113730.3.8.22.7 NAME 'ipaCertMapPriority' DESC 'Rule priority' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.5' )
+objectClasses: (2.16.840.1.113730.3.8.23.1 NAME 'ipaCertMapContainer' DESC 'IPA Certificate Mapping container' AUXILIARY MUST ( ipaCertMapVersion ) X-ORIGIN 'IPA v4.5' )
+objectClasses: (2.16.840.1.113730.3.8.23.2 NAME 'ipaCertMapConfigObject' DESC 'IPA Certificate Mapping global config options' SUP top STRUCTURAL MAY ipaCertMapPromptUsername X-ORIGIN 'IPA v4.5' )
+objectClasses: (2.16.840.1.113730.3.8.23.3 NAME 'ipaCertMapRule' DESC 'IPA Certificate Mapping rule' SUP top STRUCTURAL MUST cn MAY ( description $ ipaCertMapIssuer $ ipaCertMapMapRule $ ipaCertMapMatchRule $ associatedDomain $ ipaCertMapPriority $ ipaEnabledFlag ) X-ORIGIN 'IPA v4.5' )
+objectClasses: (2.16.840.1.113730.3.8.23.4 NAME 'ipaCertMapObject' DESC 'IPA Object for Certificate Mapping' AUXILIARY MAY ipaCertMapData X-ORIGIN 'IPA v4.5' )
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index 10de84d..6d07aec 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -26,6 +26,7 @@ dist_app_DATA =				\
 	70topology.ldif			\
 	71idviews.ldif			\
 	72domainlevels.ldif			\
+	73certmap.ldif			\
 	bootstrap-template.ldif		\
 	ca-topology.uldif		\
 	caJarSigningCert.cfg.template	\
diff --git a/install/updates/73-certmap.update b/install/updates/73-certmap.update
new file mode 100644
index 0000000..ede7d9b
--- /dev/null
+++ b/install/updates/73-certmap.update
@@ -0,0 +1,27 @@
+# Configuration for Certificate Identity Mapping
+dn: cn=certmap,cn=ipa,cn=etc,$SUFFIX
+default:objectclass: top
+default:objectclass: nsContainer
+default:objectclass: ipaConfigObject
+default:objectclass: ipaCertMapContainer
+default:objectclass: ipaCertMapConfigObject
+default:cn: certmap
+default:ipaconfigstring: CertMapVersion 1
+default:ipacertmapversion: 1
+default:ipaCertMapPromptUsername: FALSE
+
+dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,$SUFFIX
+default:objectclass: top
+default:objectclass: nsContainer
+default:cn: certmaprules
+
+# Certificate Identity Mapping Administrators
+dn: cn=Certificate Identity Mapping Administrators,cn=privileges,cn=pbac,$SUFFIX
+default:objectClass: top
+default:objectClass: groupofnames
+default:objectClass: nestedgroup
+default:cn: Certificate Identity Mapping Administrators
+default:description: Certificate Identity Mapping Administrators
+
+dn: $SUFFIX
+add:aci: (targetattr = "ipacertmapdata")(targattrfilters="add=objectclass:(objectclass=ipacertmapobject)")(version 3.0;acl "selfservice:Users can manage their own X.509 certificate identity mappings";allow (write) userdn = "ldap:///self";;)
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index e8a55e1..0ff0edb 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -61,6 +61,7 @@ app_DATA =				\
 	72-domainlevels.update		\
 	73-custodia.update		\
 	73-winsync.update		\
+	73-certmap.update		\
 	90-post_upgrade_plugins.update	\
 	$(NULL)
 
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 81643da..7f7a3ea 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -122,6 +122,8 @@
     ('container_dnsservers', DN(('cn', 'servers'), ('cn', 'dns'))),
     ('container_custodia', DN(('cn', 'custodia'), ('cn', 'ipa'), ('cn', 'etc'))),
     ('container_sysaccounts', DN(('cn', 'sysaccounts'), ('cn', 'etc'))),
+    ('container_certmap', DN(('cn', 'certmap'), ('cn', 'ipa'), ('cn', 'etc'))),
+    ('container_certmaprules', DN(('cn', 'certmaprules'), ('cn', 'certmap'), ('cn', 'ipa'), ('cn', 'etc'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 5a28026..91ea758 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -70,6 +70,7 @@
                     "70topology.ldif",
                     "71idviews.ldif",
                     "72domainlevels.ldif",
+                    "73certmap.ldif",
                     "15rfc2307bis.ldif",
                     "15rfc4876.ldif")
 
diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py
index 85ad417..7642640 100644
--- a/ipaserver/plugins/baseuser.py
+++ b/ipaserver/plugins/baseuser.py
@@ -134,7 +134,7 @@ class baseuser(LDAPObject):
     object_class_config = 'ipauserobjectclasses'
     possible_objectclasses = [
         'meporiginentry', 'ipauserauthtypeclass', 'ipauser',
-        'ipatokenradiusproxyuser'
+        'ipatokenradiusproxyuser', 'ipacertmapobject'
     ]
     disallow_object_classes = ['krbticketpolicyaux']
     permission_filter_objectclasses = ['posixaccount']
@@ -146,7 +146,8 @@ class baseuser(LDAPObject):
         'memberofindirect', 'ipauserauthtype', 'userclass',
         'ipatokenradiusconfiglink', 'ipatokenradiususername',
         'krbprincipalexpiration', 'usercertificate;binary',
-        'krbprincipalname', 'krbcanonicalname'
+        'krbprincipalname', 'krbcanonicalname',
+        'ipacertmapdata'
     ]
     search_display_attributes = [
         'uid', 'givenname', 'sn', 'homedirectory', 'krbcanonicalname',
@@ -360,6 +361,12 @@ class baseuser(LDAPObject):
             label=_('Certificate'),
             doc=_('Base-64 encoded user certificate'),
         ),
+        Str('ipacertmapdata*',
+            cli_name='data',
+            label=_('Certificate mapping data'),
+            doc=_('Certificate mapping data'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
     )
 
     def normalize_and_validate_email(self, email, config=None):
diff --git a/ipaserver/plugins/certmap.py b/ipaserver/plugins/certmap.py
new file mode 100644
index 0000000..14f73f8
--- /dev/null
+++ b/ipaserver/plugins/certmap.py
@@ -0,0 +1,345 @@
+import six
+
+from ipalib import api, errors
+from ipalib.parameters import Bool, DNParam, Flag, Int, Str
+from ipalib.plugable import Registry
+from ipalib.util import validate_domain_name
+from .baseldap import (
+    LDAPCreate,
+    LDAPDelete,
+    LDAPObject,
+    LDAPQuery,
+    LDAPRetrieve,
+    LDAPSearch,
+    LDAPUpdate,
+    pkey_to_value)
+from ipalib import _, ngettext
+from ipalib import output
+
+
+if six.PY3:
+    unicode = str
+
+__doc__ = _("""
+Certificate Identity Mapping
+""") + _("""
+Manage Certificate Identity Mapping configuration and rules.
+""") + _("""
+IPA supports the use of certificates for authentication. Certificates can
+either be stored in the user entry (full certificate in the usercertificate
+attribute), or simply linked to the user entry through a mapping.
+This code enables the management of the rules allowing to link a 
+certificate to a user entry.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ Display the Certificate Identity Mapping global configuration:
+   ipa certmapconfig-show
+""") +_("""
+ Modify Certificate Identity Mapping global configuration:
+   ipa certmapconfig-mod --promptusername=TRUE
+""") +_("""
+ Create a new Certificate Identity Mapping Rule:
+   ipa certmaprule-add myrule --desc="Link certificate with subject and issuer"
+""") +_("""
+ Modify a Certificate Identity Mapping Rule:
+   ipa certmaprule-mod myrule --maprule="<ALT-SEC-ID-I-S:altSecurityIdentities>"
+""") +_("""
+ Disable a Certificate Identity Mapping Rule:
+   ipa certmaprule-disable myrule
+""") +_("""
+ Enable a Certificate Identity Mapping Rule:
+   ipa certmaprule-enable myrule
+""") +_("""
+ Display information about a Certificate Identity Mapping Rule:
+   ipa certmaprule-show myrule
+""") +_("""
+ Find all Certificate Identity Mapping Rules with the specified domain:
+   ipa certmaprule-find --domain example.com
+""") +_("""
+ Delete a Certificate Identity Mapping Rule:
+   ipa certmaprule-del myrule
+""")
+
+register = Registry()
+
+def _domain_name_validator(ugettext, value):
+    try:
+        validate_domain_name(value)
+    except ValueError as e:
+        return unicode(e)
+
+def _domain_name_normalizer(d):
+    return d.lower().rstrip('.')
+
+
+@register()
+class certmapconfig(LDAPObject):
+    """
+    Certificate Identity Mapping configuration object
+    """
+    object_name = _('Certificate Identity Mapping configuration options')
+    default_attributes = ['ipacertmappromptusername']
+
+    container_dn = api.env.container_certmap
+
+    label = _('Certificate Identity Mapping Global Configuration')
+    label_singular = _('Certificate Identity Mapping Global Configuration')
+
+    takes_params = (
+        Bool('ipacertmappromptusername',
+            cli_name='promptusername',
+            label=_('Prompt for the username'),
+            doc=_('Prompt for the username when multiple identities are mapped to a certificate'),
+        ),
+    )
+
+    permission_filter_objectclasses = ['ipacertmapconfigobject']
+    managed_permissions = {
+        'System: Read Certmap Configuration': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'ipacertmappromptusername',
+                'cn',
+            },
+        },
+        'System: Modify Certmap Configuration': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'ipacertmappromptusername',
+            },
+            'default_privileges': {
+                'Certificate Identity Mapping Administrators'},
+        },
+    }
+
+
+@register()
+class certmapconfig_mod(LDAPUpdate):
+    __doc__ = _('Modify Certificate Identity Mapping configuration.')
+
+
+@register()
+class certmapconfig_show(LDAPRetrieve):
+    __doc__ = _('Show the current Certificate Identity Mapping configuration.')
+
+
+@register()
+class certmaprule(LDAPObject):
+    """
+    Certificate Identity Mapping Rules
+    """
+
+    label = _('Certificate Identity Mapping Rules')
+    label_singular = _('Certificate Identity Mapping Rule')
+
+    object_name = _('Certificate Identity Mapping Rule')
+    object_name_plural = _('Certificate Identity Mapping Rules')
+    object_class = ['ipacertmaprule']
+
+    container_dn = api.env.container_certmaprules
+    default_attributes = [
+        'cn', 'description', 'ipacertmapissuer',
+        'ipacertmapmaprule',
+        'ipacertmapmatchrule',
+        'associateddomain',
+        'ipacertmappriority',
+        'ipaenabledflag'
+    ]
+    search_attributes = [
+        'cn', 'description', 'ipacertmapissuer',
+        'ipacertmapmaprule',
+        'ipacertmapmatchrule',
+        'associateddomain',
+        'ipacertmappriority',
+        'ipaenabledflag'
+    ]
+
+
+    takes_params = (
+        Str('cn',
+            cli_name='rulename',
+            primary_key=True,
+            label=_('Rule name'),
+            doc=_('Certificate Identity Mapping Rule name'),
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Certificate Identity Mapping Rule description'),
+        ),
+        DNParam('ipacertmapissuer?',
+            cli_name='issuer',
+            label=_('Issuer'),
+            doc=_('LDAP DN of the certificate issuer (CN=Certificate Authority,O=DOMAIN.COM)'),
+        ),
+        Str('ipacertmapmaprule?',
+            cli_name='maprule',
+            label=_('Mapping rule'),
+            doc=_('Rule used to map the certificate with a user entry'),
+        ),
+        Str('ipacertmapmatchrule?',
+            cli_name='matchrule',
+            label=_('Matching rule'),
+            doc=_('Rule used to check if a certificate can be used for authentication'),
+        ),
+        Str('associateddomain*',
+            _domain_name_validator,
+            normalizer=_domain_name_normalizer,
+            cli_name='domain',
+            label=_('Domain name'),
+            doc=_('Domain where the user entry will be searched'),
+        ),
+        Int('ipacertmappriority?',
+            cli_name='priority',
+            label=_('Priority'),
+            doc=_('Priority of the rule (higher number means lower priority'),
+            minvalue=0,
+        ),
+        Flag('ipaenabledflag?',
+            label=_('Enabled'),
+            flags=['no_option'],
+            default=True
+        ),
+    )
+
+    permission_filter_objectclasses = ['ipacertmaprule']
+    managed_permissions = {
+        'System: Add Certmap Rules': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermright': {'add'},
+            'default_privileges': {
+                'Certificate Identity Mapping Administrators'},
+        },
+        'System: Read Certmap Rules': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'objectclass', 'cn', 'description', 'ipacertmapissuer',
+                'ipacertmapmaprule', 'ipacertmapmatchrule', 'associateddomain',
+                'ipacertmappriority', 'ipaenabledflag',
+            },
+            'default_privileges': {
+                'Certificate Identity Mapping Administrators'},
+        },
+        'System: Delete Certmap Rules': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermright': {'delete'},
+            'default_privileges': {
+                'Certificate Identity Mapping Administrators'},
+        },
+        'System: Modify Certmap Rules': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'objectclass', 'cn', 'description', 'ipacertmapissuer',
+                'ipacertmapmaprule', 'ipacertmapmatchrule', 'associateddomain',
+                'ipacertmappriority', 'ipaenabledflag',
+            },
+            'default_privileges': {
+                'Certificate Identity Mapping Administrators'},
+        },
+    }
+
+    def get_dn(self, *keys, **options):
+        rulename = keys[-1]
+        dn = super(certmaprule, self).get_dn(rulename, **options)
+        return dn
+
+
+@register()
+class certmaprule_add(LDAPCreate):
+    __doc__ = _('Create a new Certificate Identity Mapping Rule.')
+
+    msg_summary = _('Added Certificate Identity Mapping Rule "%(value)s"')
+
+
+@register()
+class certmaprule_mod(LDAPUpdate):
+    __doc__ = _('Modify a Certificate Identity Mapping Rule.')
+
+    msg_summary = _('Modified Certificate Identity Mapping Rule "%(value)s"')
+
+
+@register()
+class certmaprule_find(LDAPSearch):
+    __doc__ = _('Search for Certificate Identity Mapping Rules.')
+
+    msg_summary = ngettext(
+        '%(count)d Certificate Identity Mapping Rule matched',
+        '%(count)d Certificate Identity Mapping Rules matched', 0
+    )
+
+
+@register()
+class certmaprule_show(LDAPRetrieve):
+    __doc__ = _('Display information about a Certificate Identity Mapping Rule.')
+
+
+@register()
+class certmaprule_del(LDAPDelete):
+    __doc__ = _('Delete a Certificate Identity Mapping Rule.')
+
+    msg_summary = _('Deleted Certificate Identity Mapping Rule "%(value)s"')
+
+
+@register()
+class certmaprule_enable(LDAPQuery):
+    __doc__ = _('Enable a Certificate Identity Mapping Rule.')
+
+    msg_summary = _('Enabled Certificate Identity Mapping Rule "%(value)s"')
+    has_output = output.standard_value
+
+    def execute(self, cn, **options):
+        ldap = self.obj.backend
+
+        dn = self.obj.get_dn(cn)
+        try:
+            entry_attrs = ldap.get_entry(dn, ['ipaenabledflag'])
+        except errors.NotFound:
+            self.obj.handle_not_found(cn)
+
+        entry_attrs['ipaenabledflag'] = ['TRUE']
+
+        try:
+            ldap.update_entry(entry_attrs)
+        except errors.EmptyModlist:
+            pass
+
+        return dict(
+            result=True,
+            value=pkey_to_value(cn, options),
+        )
+
+
+@register()
+class certmaprule_disable(LDAPQuery):
+    __doc__ = _('Disable a Certificate Identity Mapping Rule.')
+
+    msg_summary = _('Disabled Certificate Identity Mapping Rule "%(value)s"')
+    has_output = output.standard_value
+
+    def execute(self, cn, **options):
+        ldap = self.obj.backend
+
+        dn = self.obj.get_dn(cn)
+        try:
+            entry_attrs = ldap.get_entry(dn, ['ipaenabledflag'])
+        except errors.NotFound:
+            self.obj.handle_not_found(cn)
+
+        entry_attrs['ipaenabledflag'] = ['FALSE']
+
+        try:
+            ldap.update_entry(entry_attrs)
+        except errors.EmptyModlist:
+            pass
+
+        return dict(
+            result=True,
+            value=pkey_to_value(cn, options),
+        )
+
diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py
index 6440548..dea8173 100644
--- a/ipaserver/plugins/user.py
+++ b/ipaserver/plugins/user.py
@@ -22,12 +22,13 @@
 from time import gmtime, strftime
 import posixpath
 import os
-
+from ldap.dn import str2dn, dn2str
 import six
 
 from ipalib import api
 from ipalib import errors
-from ipalib import Bool, Flag, Str
+from ipalib import Bool, Bytes, DNParam, Flag, Str
+from ipalib import x509
 from .baseuser import (
     baseuser,
     baseuser_add,
@@ -64,6 +65,7 @@
 from ipapython.dn import DN
 from ipapython.ipautil import ipa_generate_password, TMP_PWD_ENTROPY_BITS
 from ipalib.capabilities import client_has_capability
+from ipaserver.plugins.service import validate_certificate
 
 if api.env.in_server:
     from ipaserver.plugins.ldap2 import ldap2
@@ -179,6 +181,7 @@ class user(baseuser):
                 'secretary', 'usercertificate',
                 'usersmimecertificate', 'x500uniqueidentifier',
                 'inetuserhttpurl', 'inetuserstatus',
+                'ipacertmapdata',
             },
             'fixup_function': fix_addressbook_permission_bindrule,
         },
@@ -366,6 +369,13 @@ class user(baseuser):
             },
             'default_privileges': {'PassSync Service'},
         },
+        'System: Manage User Certificate Mappings': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {'ipacertmapdata', 'objectclass'},
+            'default_privileges': {
+                'Certificate Identity Mapping Administrators'
+            },
+        },
     }
 
     takes_params = baseuser.takes_params + (
@@ -1200,6 +1210,165 @@ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         return dn
 
 
+def _convert_to_x500(name):
+    """
+    Converts a (ipa) DN into a string representation following X500 order
+    """
+    if name:
+        dn = str2dn(str(name))
+        dn.reverse()
+        return dn2str(dn)
+    return name
+
+def _build_mapdata(subject, issuer):
+    issuer = _convert_to_x500(issuer)
+    subject = _convert_to_x500(subject)
+    return u'X509:{issuer}{subject}'.format(
+        issuer='<I>{}'.format(issuer) if issuer else '',
+        subject='<S>{}'.format(subject) if subject else '',
+        )
+
+def _convert_options_to_certmap(options):
+    """
+    Converts options to ipacertmapdata
+
+    When --subject --issuer or --usercertificate options are used,
+    the value for ipacertmapdata is built from extracting subject and issuer,
+    converting their values to X500 ordering and using the format
+    X509:<I>issuer<S>subject
+    For instance:
+    X509:<I>O=DOMAIN,CN=Certificate Authority<S>O=DOMAIN,CN=user
+    A list of values can be returned if --usercertificate is used multiple
+    times, or in conjunction with --subject --issuer.
+    """
+    data = []
+    if 'ipacertmapdata' in options:
+        for item in options['ipacertmapdata']:
+            data.append(item)
+
+    if 'issuer' in options or 'subject' in options:
+        issuer = options.get('issuer')
+        subject = options.get('subject')
+        data.append(_build_mapdata(subject, issuer))
+
+    if 'usercertificate' in options:
+        for dercert in options.get('usercertificate'):
+            cert = x509.load_certificate(dercert, x509.DER)
+            issuer = DN(cert.issuer)
+            subject = DN(cert.subject)
+            data.append(_build_mapdata(subject, issuer))
+
+    return data
+
+
+certmap_options = (
+    DNParam('issuer?',
+        cli_name='issuer',
+        label=_('Issuer'),
+        doc=_('Issuer of the certificate'),
+        flags=['virtual_attribute', 'no_create', 'no_update']
+    ),
+    DNParam('subject?',
+        cli_name='subject',
+        label=_('Subject'),
+        doc=_('Subject of the certificate'),
+        flags=['virtual_attribute', 'no_create', 'no_update']
+    ),
+    Bytes('usercertificate*', validate_certificate,
+        cli_name='certificate',
+        label=_('Certificate'),
+        doc=_('Base-64 encoded user certificate'),
+    ),
+)
+
+@register()
+class user_add_certmap(LDAPAddAttributeViaOption):
+    __doc__ = _("Add one or more certificate mappings to the user entry.")
+    msg_summary = _('Added certificate mappings to user "%(value)s"')
+
+    attribute = 'ipacertmapdata'
+    takes_options = certmap_options
+
+    def get_options(self):
+        # ipacertmapdata is not mandatory as it can be built
+        # from the values subject+issuer or from reading usercertificate
+        for option in super(user_add_certmap, self).get_options():
+            if option.name in ['ipacertmapdata']:
+                yield option.clone(required=False, alwaysask=False)
+            else:
+                yield option.clone()
+
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+                     **options):
+        # The 3 valid calls are
+        # --subject xx --issuer yy
+        # --certificate
+        # --data
+        # Check that at least one of the 3 formats is used
+        if 'issuer' not in options and \
+           'subject' not in options and \
+           'ipacertmapdata' not in options and \
+           'usercertificate' not in options:
+            raise errors.RequirementError(name='data')
+
+        # The objectclass ipacertmapobject may not be present on
+        # existing user entries. We need to add it if we define a new
+        # value for ipacertmapdata
+        if 'objectclass' not in entry_attrs:
+            entry_attrs_old = ldap.get_entry(dn, ['objectclass'])
+        objclasses_lc = [x.lower() for x in entry_attrs_old['objectclass']]
+        if 'ipacertmapobject' not in objclasses_lc:
+            entry_attrs['objectclass'] = ['ipacertmapobject']
+
+        entry_attrs[self.attribute] = _convert_options_to_certmap(options)
+
+        # if the command is called with --subject --issuer or --certificate
+        # we need to add ipacertmapdata to the attrs_list in order to
+        # display the resulting value in the command output
+        if 'ipacertmapdata' not in attrs_list:
+            attrs_list.append('ipacertmapdata')
+
+        return dn
+
+
+@register()
+class user_remove_certmap(LDAPRemoveAttributeViaOption):
+    __doc__ = _("Remove one or more certificate mappings from the user entry.")
+    msg_summary = _('Removed certificate mappings from user "%(value)s"')
+
+    attribute = 'ipacertmapdata'
+    takes_options = certmap_options
+
+    def get_options(self):
+        # ipacertmapdata is not mandatory as it can be built
+        # from the values subject+issuer or from reading usercertificate
+        for option in super(user_remove_certmap, self).get_options():
+            if option.name in ['ipacertmapdata']:
+                yield option.clone(required=False, alwaysask=False)
+            else:
+                yield option.clone()
+
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+                     **options):
+        if 'issuer' not in options and \
+           'subject' not in options and \
+           'ipacertmapdata' not in options and \
+           'usercertificate' not in options:
+            raise errors.RequirementError(name='data')
+
+        entry_attrs[self.attribute] = _convert_options_to_certmap(options)
+
+        # if the command is called with --subject --issuer or --certificate
+        # we need to add ipacertmapdata to the attrs_list in order to
+        # display the resulting value in the command output
+        if 'ipacertmapdata' not in attrs_list:
+            attrs_list.append('ipacertmapdata')
+
+        return dn
+
+
 @register()
 class user_add_manager(baseuser_add_manager):
     __doc__ = _("Add a manager to the user entry")

From 1566d5cb6ed51ea36e7bcefa20b0f1f7005f7b38 Mon Sep 17 00:00:00 2001
From: David Kupka <dku...@redhat.com>
Date: Fri, 13 Jan 2017 13:17:35 +0100
Subject: [PATCH 2/4] test_xmlrpc: tracker: Add enable and disable methods to
 tracker

Prepare tracker for easier testing of *-{en,dis}able commands.
---
 ipatests/test_xmlrpc/tracker/base.py | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/ipatests/test_xmlrpc/tracker/base.py b/ipatests/test_xmlrpc/tracker/base.py
index aa88e6b..d8cd3a6 100644
--- a/ipatests/test_xmlrpc/tracker/base.py
+++ b/ipatests/test_xmlrpc/tracker/base.py
@@ -198,6 +198,14 @@ def make_update_command(self, updates):
         """Make function that modifies the entry using ${CMD}_mod"""
         raise NotImplementedError(self._override_me_msg)
 
+    def make_enable_command(self):
+        """Make function that enables the entry using ${CMD}_enable"""
+        raise NotImplementedError(self._override_me_msg)
+
+    def make_disable_command(self):
+        """Make function that disables the entry using ${CMD}_disable"""
+        raise NotImplementedError(self._override_me_msg)
+
     def create(self):
         """Helper function to create an entry and check the result"""
         self.track_create()
@@ -285,3 +293,21 @@ def update(self, updates, expected_updates=None):
     def check_update(self, result, extra_keys=()):
         """Check the plugin's `mod` command result"""
         raise NotImplementedError(self._override_me_msg)
+
+    def enable(self):
+        command = self.make_enable_command()
+        result = command()
+        self.check_enable(result)
+
+    def check_enable(self, result):
+        """Check the plugin's `enable` command result"""
+        raise NotImplementedError(self._override_me_msg)
+
+    def disable(self):
+        command = self.make_disable_command()
+        result = command()
+        self.check_disable(result)
+
+    def check_disable(self, result):
+        """Check the plugin's `disable` command result"""
+        raise NotImplementedError(self._override_me_msg)

From e2f085f769f99b9b95684b41d18da653fd9ec690 Mon Sep 17 00:00:00 2001
From: David Kupka <dku...@redhat.com>
Date: Fri, 13 Jan 2017 13:22:45 +0100
Subject: [PATCH 3/4] test: certmap: Add basic tests for certmaprule commands.

https://fedorahosted.org/freeipa/ticket/6542
---
 ipatests/test_xmlrpc/objectclasses.py          |   5 +
 ipatests/test_xmlrpc/test_certmap_plugin.py    | 107 ++++++++++++++++
 ipatests/test_xmlrpc/tracker/certmap_plugin.py | 167 +++++++++++++++++++++++++
 3 files changed, 279 insertions(+)
 create mode 100644 ipatests/test_xmlrpc/test_certmap_plugin.py
 create mode 100644 ipatests/test_xmlrpc/tracker/certmap_plugin.py

diff --git a/ipatests/test_xmlrpc/objectclasses.py b/ipatests/test_xmlrpc/objectclasses.py
index 1ea020b..0a15a21 100644
--- a/ipatests/test_xmlrpc/objectclasses.py
+++ b/ipatests/test_xmlrpc/objectclasses.py
@@ -227,3 +227,8 @@
     u'top',
     u'ipaca',
 ]
+
+certmaprule = [
+    u'top',
+    u'ipacertmaprule',
+]
diff --git a/ipatests/test_xmlrpc/test_certmap_plugin.py b/ipatests/test_xmlrpc/test_certmap_plugin.py
new file mode 100644
index 0000000..9343f9a
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_certmap_plugin.py
@@ -0,0 +1,107 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+#
+
+import itertools
+import pytest
+
+from ipapython.dn import DN
+from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test
+from ipatests.test_xmlrpc.tracker.certmap_plugin import CertmapruleTracker
+
+certmaprule_create_params = {
+        u'cn': u'test_rule',
+        u'description': u'Certificate mapping and matching rule for test '
+                        u'purposes',
+        u'ipacertmapissuer': DN('CN=CA,O=EXAMPLE.ORG'),
+        u'ipacertmapmaprule': u'arbitrary free-form mapping rule defined and '
+                              u'consumed by SSSD',
+        u'ipacertmapmatchrule': u'arbitrary free-form matching rule defined '
+                                u'and consumed by SSSD',
+        u'associateddomain': u'example.org',
+        u'ipacertmappriority': u'1',
+}
+
+certmaprule_update_params = {
+        u'description': u'Changed description',
+        u'ipacertmapissuer': DN('CN=Changed CA,O=OTHER.ORG'),
+        u'ipacertmapmaprule': u'changed arbitrary mapping rule',
+        u'ipacertmapmatchrule': u'changed arbitrary maching rule',
+        u'associateddomain': u'changed.example.org',
+        u'ipacertmappriority': u'5',
+}
+
+certmaprule_optional_params = (
+    'description',
+    'ipacertmapissuer',
+    'ipacertmapmaprule',
+    'ipacertmapmatchrule',
+    'ipaassociateddomain',
+    'ipacertmappriority',
+)
+
+def dontfill_idfn(dont_fill):
+    return u"dont_fill=({})".format(', '.join([
+        u"{}".format(d) for d in dont_fill
+    ]))
+
+
+def update_idfn(update):
+    return ', '.join(["{}: {}".format(k, v) for k, v in update.items()])
+
+
+@pytest.fixture(scope='class')
+def certmap_rule(request):
+    tracker = CertmapruleTracker(**certmaprule_create_params)
+    return tracker.make_fixture(request)
+
+
+class TestCRUD(XMLRPC_test):
+    @pytest.mark.parametrize(
+        'dont_fill',
+        itertools.chain(*[
+            itertools.combinations(certmaprule_optional_params, l)
+            for l in range(len(certmaprule_optional_params)+1)
+        ]),
+        ids=dontfill_idfn,
+    )
+    def test_create(self, dont_fill, certmap_rule):
+        certmap_rule.ensure_missing()
+        try:
+            certmap_rule.create(dont_fill)
+        finally:
+            certmap_rule.ensure_missing()
+
+    def test_retrieve(self, certmap_rule):
+        certmap_rule.ensure_exists()
+        certmap_rule.retrieve()
+
+    def test_find(self, certmap_rule):
+        certmap_rule.ensure_exists()
+        certmap_rule.find()
+
+    @pytest.mark.parametrize('update', [
+            dict(u) for l in range(1, len(certmaprule_update_params)+1)
+            for u in itertools.combinations(
+                certmaprule_update_params.items(), l)
+        ],
+        ids=update_idfn,
+    )
+    def test_update(self, update, certmap_rule):
+        certmap_rule.ensure_missing()
+        certmap_rule.ensure_exists()
+        certmap_rule.update(update, {o: [v] for o, v in update.items()})
+
+    def test_delete(self, certmap_rule):
+        certmap_rule.ensure_exists()
+        certmap_rule.delete()
+
+
+class TestEnableDisable(XMLRPC_test):
+    def test_disable(self, certmap_rule):
+        certmap_rule.ensure_exists()
+        certmap_rule.disable()
+
+    def test_enable(self, certmap_rule):
+        certmap_rule.ensure_exists()
+        certmap_rule.enable()
diff --git a/ipatests/test_xmlrpc/tracker/certmap_plugin.py b/ipatests/test_xmlrpc/tracker/certmap_plugin.py
new file mode 100644
index 0000000..76022bf
--- /dev/null
+++ b/ipatests/test_xmlrpc/tracker/certmap_plugin.py
@@ -0,0 +1,167 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+#
+
+from ipapython.dn import DN
+from ipatests.test_xmlrpc.tracker.base import Tracker
+from ipatests.test_xmlrpc import objectclasses
+from ipatests.util import assert_deepequal
+
+
+class CertmapruleTracker(Tracker):
+    """ Tracker for testin certmaprule plugin """
+    retrieve_keys = {
+        u'dn',
+        u'cn',
+        u'description',
+        u'ipacertmapissuer',
+        u'ipacertmapmaprule',
+        u'ipacertmapmatchrule',
+        u'associateddomain',
+        u'ipacertmappriority',
+        u'ipaenabledflag'
+    }
+    retrieve_all_keys = retrieve_keys | {u'objectclass'}
+    create_keys = retrieve_keys | {u'objectclass'}
+    update_keys = retrieve_keys - {u'dn'}
+
+    def __init__(self, cn, description, ipacertmapissuer, ipacertmapmaprule,
+                 ipacertmapmatchrule, associateddomain, ipacertmappriority,
+                 default_version=None):
+        super(CertmapruleTracker, self).__init__(
+            default_version=default_version)
+
+        self.dn = DN((u'cn', cn,),
+                     self.api.env.container_certmaprules,
+                     self.api.env.basedn)
+        self.options = {
+            u'description': description,
+            u'ipacertmapissuer': ipacertmapissuer,
+            u'ipacertmapmaprule': ipacertmapmaprule,
+            u'ipacertmapmatchrule': ipacertmapmatchrule,
+            u'associateddomain': associateddomain,
+            u'ipacertmappriority': ipacertmappriority,
+        }
+
+    def make_create_command(self, dont_fill=()):
+        kwargs = {k: v for k, v in self.options.items() if k not in dont_fill}
+
+        return self.make_command('certmaprule_add', self.name, **kwargs)
+
+    def track_create(self, dont_fill=()):
+        self.attrs = {
+            'dn': self.dn,
+            'cn': [self.name],
+            'ipaenabledflag': [u'TRUE'],
+            'objectclass': objectclasses.certmaprule,
+        }
+        self.attrs.update({
+            k: [v] for k, v in self.options.items() if k not in dont_fill
+        })
+        self.exists = True
+
+    def check_create(self, result):
+        assert_deepequal(dict(
+            value=self.name,
+            summary=u'Added Certificate Identity Mapping Rule "{}"'
+                    u''.format(self.name),
+            result=self.filter_attrs(self.create_keys),
+        ), result)
+
+    def create(self, dont_fill=()):
+        self.track_create(dont_fill)
+        command = self.make_create_command(dont_fill)
+        result = command()
+        self.check_create(result)
+
+    def make_delete_command(self):
+        return self.make_command('certmaprule_del', self.name)
+
+    def check_delete(self, result):
+        assert_deepequal(
+            dict(
+                value=[self.name],
+                summary=u'Deleted Certificate Identity Mapping Rule "{}"'
+                        ''.format(self.name),
+                result=dict(failed=[]),
+            ),
+            result
+        )
+
+    def make_retrieve_command(self, all=False, raw=False):
+        return self.make_command('certmaprule_show', self.name, all=all,
+                                 raw=raw)
+
+    def check_retrieve(self, result, all=False, raw=False):
+        if all:
+            expected = self.filter_attrs(self.retrieve_all_keys)
+        else:
+            expected = self.filter_attrs(self.retrieve_keys)
+        assert_deepequal(
+            dict(
+                value=self.name,
+                summary=None,
+                result=expected,
+            ),
+            result
+        )
+
+    def make_find_command(self, *args, **kwargs):
+        return self.make_command('certmaprule_find', *args, **kwargs)
+
+    def check_find(self, result, all=False, raw=False):
+        if all:
+            expected = self.filter_attrs(self.retrieve_all_keys)
+        else:
+            expected = self.filter_attrs(self.retrieve_keys)
+        assert_deepequal(
+            dict(
+                count=1,
+                truncated=False,
+                summary=u'1 Certificate Identity Mapping Rule matched',
+                result=[expected],
+            ),
+            result
+        )
+
+    def make_update_command(self, updates):
+        return self.make_command('certmaprule_mod', self.name, **updates)
+
+    def check_update(self, result, extra_keys=()):
+        assert_deepequal(
+            dict(
+                value=self.name,
+                summary=u'Modified Certificate Identity Mapping Rule "{}"'
+                        u''.format(self.name),
+                result=self.filter_attrs(self.update_keys | set(extra_keys)),
+            ),
+            result
+        )
+
+    def make_enable_command(self):
+        return self.make_command('certmaprule_enable', self.name)
+
+    def check_enable(self, result):
+        assert_deepequal(
+            dict(
+                value=self.name,
+                summary=u'Enabled Certificate Identity Mapping Rule "{}"'
+                        u''.format(self.name),
+                result=True,
+            ),
+            result
+        )
+
+    def make_disable_command(self):
+        return self.make_command('certmaprule_disable', self.name)
+
+    def check_disable(self, result):
+        assert_deepequal(
+            dict(
+                value=self.name,
+                summary=u'Disabled Certificate Identity Mapping Rule "{}"'
+                        u''.format(self.name),
+                result=True,
+            ),
+            result
+        )

From f675c08b04f07e0f311fe2c09361bc7dab96aac3 Mon Sep 17 00:00:00 2001
From: David Kupka <dku...@redhat.com>
Date: Tue, 24 Jan 2017 16:21:54 +0100
Subject: [PATCH 4/4] tests: certmap: Test ACI works as expected

https://fedorahosted.org/freeipa/ticket/6542
---
 ipatests/test_xmlrpc/test_certmap_plugin.py | 260 ++++++++++++++++++++++++++++
 1 file changed, 260 insertions(+)

diff --git a/ipatests/test_xmlrpc/test_certmap_plugin.py b/ipatests/test_xmlrpc/test_certmap_plugin.py
index 9343f9a..00f5b69 100644
--- a/ipatests/test_xmlrpc/test_certmap_plugin.py
+++ b/ipatests/test_xmlrpc/test_certmap_plugin.py
@@ -2,12 +2,18 @@
 # Copyright (C) 2017  FreeIPA Contributors see COPYING for license
 #
 
+from contextlib import contextmanager
 import itertools
+from nose.tools import assert_raises
 import pytest
 
+from ipalib import api, errors
 from ipapython.dn import DN
 from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test
 from ipatests.test_xmlrpc.tracker.certmap_plugin import CertmapruleTracker
+from ipatests.util import assert_deepequal
+from ipatests.util import change_principal, unlock_principal_password
+
 
 certmaprule_create_params = {
         u'cn': u'test_rule',
@@ -40,6 +46,17 @@
     'ipacertmappriority',
 )
 
+certmaprule_permissions = {
+    u'C': u'System: Add Certmap Rules',
+    u'R': u'System: Read Certmap Rules',
+    u'U': u'System: Modify Certmap Rules',
+    u'D': u'System: Delete Certmap Rules',
+}
+
+CERTMAP_USER = u'cuser'
+CERTMAP_PASSWD = 'Secret123'
+
+
 def dontfill_idfn(dont_fill):
     return u"dont_fill=({})".format(', '.join([
         u"{}".format(d) for d in dont_fill
@@ -105,3 +122,246 @@ def test_disable(self, certmap_rule):
     def test_enable(self, certmap_rule):
         certmap_rule.ensure_exists()
         certmap_rule.enable()
+
+
+class Result(Exception):
+    def __init__(self, value):
+        self.value = value
+
+
+@contextmanager
+def execute_with_expected(user, password, perms, exps, ok_expected=None):
+    """
+    Run command as specified user. Check exception or return value
+    according provided rules.
+
+    @param user     Change to this user before calling the command
+    @param password User to change user
+    @param perms    User has those permissions
+    @param exps     Iterable containing tuple
+                    (permission, exception_class, expected_result,)
+                    If permission is missing command must raise exception of
+                    exception_class. If exception class is None command must
+                    raise Result(expected_result)
+    @param ok_expected  When no permission is missing command must raise
+                        Result(ok_expected)
+    """
+    with change_principal(user, password):
+        for perm, exception, expected in exps:
+            if perm not in perms:
+                if exception:
+                    with assert_raises(exception):
+                        yield
+                else:
+                    try:
+                        yield
+                    except Result as got:
+                        assert_deepequal(expected, got.value)
+                    else:
+                        if ok_expected:
+                            assert("Command didn't raise Result")
+                break
+        else:
+            try:
+                yield
+            except Result as got:
+                if ok_expected:
+                    assert_deepequal(ok_expected, got.value)
+            else:
+                if ok_expected:
+                    assert("Command didn't raise Result")
+
+
+def permissions_idfn(perms):
+    i = []
+    for short_name, long_name in certmaprule_permissions.items():
+        if long_name in perms:
+            i.append(short_name)
+        else:
+            i.append('-')
+    return ''.join(i)
+
+
+@pytest.fixture(
+    scope='class',
+    params=itertools.chain(*[
+            itertools.combinations(certmaprule_permissions.values(), l)
+            for l in range(len(certmaprule_permissions.values())+1)
+    ]),
+    ids=permissions_idfn,
+)
+def certmap_user_permissions(request):
+    tmp_password = u'Initial123'
+
+    priv_name = u'test_certmap_privilege'
+    role_name = u'test_certmap_role'
+
+    api.Command.user_add(CERTMAP_USER, givenname=u'Certmap', sn=u'User',
+                         userpassword=tmp_password)
+    unlock_principal_password(CERTMAP_USER, tmp_password,
+                              CERTMAP_PASSWD)
+
+    api.Command.privilege_add(priv_name)
+    for perm_name in request.param:
+        api.Command.privilege_add_permission(priv_name, permission=perm_name)
+    api.Command.role_add(role_name)
+    api.Command.role_add_privilege(role_name, privilege=priv_name)
+    api.Command.role_add_member(role_name, user=CERTMAP_USER)
+
+    def finalize():
+        try:
+            api.Command.user_del(CERTMAP_USER)
+        except Exception:
+            pass
+        try:
+            api.Command.role_del(role_name)
+        except Exception:
+            pass
+        try:
+            api.Command.privilege_del(priv_name)
+        except Exception:
+            pass
+    request.addfinalizer(finalize)
+
+    return request.param
+
+
+class TestPermission(XMLRPC_test):
+    def test_create(self, certmap_rule, certmap_user_permissions):
+        certmap_rule.ensure_missing()
+
+        with execute_with_expected(
+            CERTMAP_USER,
+            CERTMAP_PASSWD,
+            certmap_user_permissions,
+            [
+                (u'System: Add Certmap Rules', errors.ACIError, None,),
+                (u'System: Read Certmap Rules', errors.NotFound, None,),
+            ],
+        ):
+            certmap_rule.create(),
+
+        # Tracker sets 'exists' to True even when the create does not
+        # succeed so ensure_missing wouldn't be reliable here
+        try:
+            certmap_rule.delete()
+        except Exception:
+            pass
+
+    def test_retrieve(self, certmap_rule, certmap_user_permissions):
+        certmap_rule.ensure_exists()
+
+        with execute_with_expected(
+            CERTMAP_USER,
+            CERTMAP_PASSWD,
+            certmap_user_permissions,
+            [
+                (u'System: Read Certmap Rules', errors.NotFound, None,),
+            ],
+        ):
+            certmap_rule.retrieve()
+
+    def test_find(self, certmap_rule, certmap_user_permissions):
+        certmap_rule.ensure_exists()
+
+        expected_without_read = {
+            u'count': 0,
+            u'result': (),
+            u'summary': u'0 Certificate Identity Mapping Rules matched',
+            u'truncated': False,
+        }
+        expected_ok = {
+            u'count': 1,
+            u'result': [{
+                k: (v,) for k, v in certmaprule_create_params.items()
+            }],
+            u'summary': u'1 Certificate Identity Mapping Rule matched',
+            u'truncated': False,
+        }
+        expected_ok[u'result'][0][u'dn'] = DN(
+            (u'cn', expected_ok[u'result'][0][u'cn'][0]),
+            api.env.container_certmaprules,
+            api.env.basedn,
+        )
+        expected_ok[u'result'][0][u'ipaenabledflag'] = (u'TRUE',)
+        with execute_with_expected(
+            CERTMAP_USER,
+            CERTMAP_PASSWD,
+            certmap_user_permissions,
+            [
+                (u'System: Read Certmap Rules', None, expected_without_read,),
+            ],
+            expected_ok,
+        ):
+            find = certmap_rule.make_find_command()
+            got = find(**{k: v for k, v in certmaprule_create_params.items()
+                          if k is not u'dn'})
+            raise Result(got)
+
+    def test_update(self, certmap_rule, certmap_user_permissions):
+        certmap_rule.ensure_missing()
+        certmap_rule.ensure_exists()
+
+        with execute_with_expected(
+            CERTMAP_USER,
+            CERTMAP_PASSWD,
+            certmap_user_permissions,
+            [
+                (u'System: Read Certmap Rules', errors.NotFound, None,),
+                (u'System: Modify Certmap Rules', errors.ACIError, None,),
+            ],
+        ):
+            certmap_rule.update(
+                certmaprule_update_params,
+                {o: [v] for o, v in certmaprule_update_params.items()},
+            )
+
+    def test_delete(self, certmap_rule, certmap_user_permissions):
+        certmap_rule.ensure_exists()
+
+        with execute_with_expected(
+            CERTMAP_USER,
+            CERTMAP_PASSWD,
+            certmap_user_permissions,
+            [
+                (u'System: Delete Certmap Rules', errors.ACIError, None,),
+            ],
+        ):
+            certmap_rule.delete()
+
+        # Tracker sets 'exists' to False even when the delete does not
+        # succeed so ensure_missing wouldn't be reliable here
+        try:
+            certmap_rule.delete()
+        except Exception:
+            pass
+
+    def test_enable(self, certmap_rule, certmap_user_permissions):
+        certmap_rule.ensure_exists()
+        certmap_rule.disable()
+
+        with execute_with_expected(
+            CERTMAP_USER,
+            CERTMAP_PASSWD,
+            certmap_user_permissions,
+            [
+                (u'System: Read Certmap Rules', errors.NotFound, None,),
+                (u'System: Modify Certmap Rules', errors.ACIError, None,),
+            ],
+        ):
+            certmap_rule.enable()
+
+    def test_disable(self, certmap_rule, certmap_user_permissions):
+        certmap_rule.ensure_exists()
+        certmap_rule.enable()
+
+        with execute_with_expected(
+            CERTMAP_USER,
+            CERTMAP_PASSWD,
+            certmap_user_permissions,
+            [
+                (u'System: Read Certmap Rules', errors.NotFound, None,),
+                (u'System: Modify Certmap Rules', errors.ACIError, None,),
+            ],
+        ):
+            certmap_rule.disable()
-- 
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