URL: https://github.com/freeipa/freeipa/pull/516
Author: flo-renaud
 Title: #516: IdM Server: list all Employees with matching Smart Card
Action: synchronized

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/516/head:pr516
git checkout pr516
From 2bf231a8dbdf5f8e1e0177093bc7559b127fdf7d 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/2] 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                            | 181 +++++++++++++++++
 VERSION.m4                         |   4 +-
 install/share/73certmap.ldif       |  14 ++
 install/share/Makefile.am          |   1 +
 install/updates/73-certmap.update  |  23 +++
 install/updates/Makefile.am        |   1 +
 ipalib/constants.py                |   2 +
 ipapython/dn.py                    |   8 +-
 ipaserver/install/dsinstance.py    |   1 +
 ipaserver/plugins/baseuser.py      | 174 ++++++++++++++++-
 ipaserver/plugins/certmap.py       | 391 +++++++++++++++++++++++++++++++++++++
 ipaserver/plugins/stageuser.py     |  16 +-
 ipaserver/plugins/user.py          |  23 ++-
 ipatests/test_ipapython/test_dn.py |  20 ++
 15 files changed, 862 insertions(+), 13 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..a36d460 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,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,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,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,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,dc=ipa,dc=example
+aci: (targetattr = "associateddomain || cn || description || 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,dc=ipa,dc=example
+aci: (targetattr = "associateddomain || cn || createtimestamp || description || entryusn || ipacertmapmaprule || ipacertmapmatchrule || ipacertmappriority || ipaenabledflag || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Read Certmap Rules";allow (compare,read,search) userdn = "ldap:///all";;)
 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 3ebebab..a8f8ff1 100644
--- a/API.txt
+++ b/API.txt
@@ -824,6 +824,116 @@ 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,11,3
+arg: Str('cn', cli_name='rulename')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: DNSNameParam('associateddomain*', cli_name='domain')
+option: Str('description?', cli_name='desc')
+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,13,4
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: DNSNameParam('associateddomain*', autofill=False, cli_name='domain')
+option: Str('cn?', autofill=False, cli_name='rulename')
+option: Str('description?', autofill=False, cli_name='desc')
+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,13,3
+arg: Str('cn', cli_name='rulename')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: DNSNameParam('associateddomain*', autofill=False, cli_name='domain')
+option: Str('delattr*', cli_name='delattr')
+option: Str('description?', autofill=False, cli_name='desc')
+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')
@@ -4762,6 +4872,20 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: stageuser_add_certmapdata/1
+args: 2,7,3
+arg: Str('uid', cli_name='login')
+arg: Str('ipacertmapdata*', alwaysask=False, cli_name='certmapdata')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Bytes('certificate*', cli_name='certificate')
+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: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: stageuser_add_manager/1
 args: 1,5,3
 arg: Str('uid', cli_name='login')
@@ -4915,6 +5039,20 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: stageuser_remove_certmapdata/1
+args: 2,7,3
+arg: Str('uid', cli_name='login')
+arg: Str('ipacertmapdata*', alwaysask=False, cli_name='certmapdata')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Bytes('certificate*', cli_name='certificate')
+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: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: stageuser_remove_manager/1
 args: 1,5,3
 arg: Str('uid', cli_name='login')
@@ -5796,6 +5934,20 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: user_add_certmapdata/1
+args: 2,7,3
+arg: Str('uid', cli_name='login')
+arg: Str('ipacertmapdata*', alwaysask=False, cli_name='certmapdata')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Bytes('certificate*', cli_name='certificate')
+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: 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')
@@ -5968,6 +6120,20 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: user_remove_certmapdata/1
+args: 2,7,3
+arg: Str('uid', cli_name='login')
+arg: Str('ipacertmapdata*', alwaysask=False, cli_name='certmapdata')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Bytes('certificate*', cli_name='certificate')
+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: 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')
@@ -6351,6 +6517,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
@@ -6706,12 +6883,14 @@ default: stageuser/1
 default: stageuser_activate/1
 default: stageuser_add/1
 default: stageuser_add_cert/1
+default: stageuser_add_certmapdata/1
 default: stageuser_add_manager/1
 default: stageuser_add_principal/1
 default: stageuser_del/1
 default: stageuser_find/1
 default: stageuser_mod/1
 default: stageuser_remove_cert/1
+default: stageuser_remove_certmapdata/1
 default: stageuser_remove_manager/1
 default: stageuser_remove_principal/1
 default: stageuser_show/1
@@ -6789,6 +6968,7 @@ default: trustdomain_mod/1
 default: user/1
 default: user_add/1
 default: user_add_cert/1
+default: user_add_certmapdata/1
 default: user_add_manager/1
 default: user_add_principal/1
 default: user_del/1
@@ -6797,6 +6977,7 @@ default: user_enable/1
 default: user_find/1
 default: user_mod/1
 default: user_remove_cert/1
+default: user_remove_certmapdata/1
 default: user_remove_manager/1
 default: user_remove_principal/1
 default: user_show/1
diff --git a/VERSION.m4 b/VERSION.m4
index 8d66718..8c93277 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, 218)
-# Last change: Remove no_option flag for nsaccountlock and add cli_name='disabled'
+define(IPA_API_VERSION_MINOR, 219)
+# 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..9c67ccb
--- /dev/null
+++ b/install/share/73certmap.ldif
@@ -0,0 +1,14 @@
+## IPA Base OID:
+##
+## Attributes:          2.16.840.1.113730.3.8.22.1.x
+## ObjectClasses:       2.16.840.1.113730.3.8.22.2.y
+##
+dn: cn=schema
+attributeTypes: (2.16.840.1.113730.3.8.22.1.1 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.1.2 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.1.3 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.1.4 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.1.5 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.22.2.1 NAME 'ipaCertMapConfigObject' DESC 'IPA Certificate Mapping global config options' AUXILIARY MAY ipaCertMapPromptUsername X-ORIGIN 'IPA v4.5' )
+objectClasses: (2.16.840.1.113730.3.8.22.2.2 NAME 'ipaCertMapRule' DESC 'IPA Certificate Mapping rule' SUP top STRUCTURAL MUST cn MAY ( description $ ipaCertMapMapRule $ ipaCertMapMatchRule $ associatedDomain $ ipaCertMapPriority $ ipaEnabledFlag ) X-ORIGIN 'IPA v4.5' )
+objectClasses: (2.16.840.1.113730.3.8.22.2.3 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 c58e1d2..bbf6ce1 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -27,6 +27,7 @@ dist_app_DATA =				\
 	70topology.ldif			\
 	71idviews.ldif			\
 	72domainlevels.ldif			\
+	73certmap.ldif			\
 	anon-princ-aci.ldif		\
 	bootstrap-template.ldif		\
 	ca-topology.uldif		\
diff --git a/install/updates/73-certmap.update b/install/updates/73-certmap.update
new file mode 100644
index 0000000..ecb3db3
--- /dev/null
+++ b/install/updates/73-certmap.update
@@ -0,0 +1,23 @@
+# Configuration for Certificate Identity Mapping
+dn: cn=certmap,$SUFFIX
+default:objectclass: top
+default:objectclass: nsContainer
+default:objectclass: ipaCertMapConfigObject
+default:cn: certmap
+default:ipaCertMapPromptUsername: FALSE
+
+dn: cn=certmaprules,cn=certmap,$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 bc78422..8789a95 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'))),
+    ('container_certmaprules', DN(('cn', 'certmaprules'), ('cn', 'certmap'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipapython/dn.py b/ipapython/dn.py
index 4e8c22b..a54629e 100644
--- a/ipapython/dn.py
+++ b/ipapython/dn.py
@@ -1155,9 +1155,15 @@ def __deepcopy__(self, memo):
     def _get_rdn(self, rdn):
         return self.RDN_type(*rdn, **{'raw': True})
 
-    def __str__(self):
+    def ldap_text(self):
         return dn2str(self.rdns)
 
+    def x500_text(self):
+        return dn2str(reversed(self.rdns))
+
+    def __str__(self):
+        return self.ldap_text()
+
     def __repr__(self):
         return "%s.%s('%s')" % (self.__module__, self.__class__.__name__, self.__str__())
 
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 99e6190..733dd40 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 75cf7d8..44adc76 100644
--- a/ipaserver/plugins/baseuser.py
+++ b/ipaserver/plugins/baseuser.py
@@ -19,14 +19,17 @@
 
 import six
 
-from ipalib import api, errors
-from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime, Bytes
+from ipalib import api, errors, x509
+from ipalib import (
+    Flag, Int, Password, Str, Bool, StrEnum, DateTime, Bytes, DNParam)
 from ipalib.parameters import Principal
 from ipalib.plugable import Registry
 from .baseldap import (
     DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete,
-    LDAPRetrieve, LDAPAddAttribute, LDAPRemoveAttribute, LDAPAddMember,
-    LDAPRemoveMember, LDAPAddAttributeViaOption, LDAPRemoveAttributeViaOption)
+    LDAPRetrieve, LDAPAddAttribute, LDAPModAttribute, LDAPRemoveAttribute,
+    LDAPAddMember, LDAPRemoveMember,
+    LDAPAddAttributeViaOption, LDAPRemoveAttributeViaOption,
+    add_missing_object_class)
 from ipaserver.plugins.service import (
    validate_certificate, validate_realm, normalize_principal)
 from ipalib.request import context
@@ -134,7 +137,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 +149,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 +364,13 @@ class baseuser(LDAPObject):
             label=_('Certificate'),
             doc=_('Base-64 encoded user certificate'),
         ),
+        Str(
+            'ipacertmapdata*',
+            cli_name='certmapdata',
+            label=_('Certificate mapping data'),
+            doc=_('Certificate mapping data'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
     )
 
     def normalize_and_validate_email(self, email, config=None):
@@ -728,3 +739,154 @@ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         self.obj.convert_usercertificate_post(entry_attrs, **options)
 
         return dn
+
+
+class ModCertMapData(LDAPModAttribute):
+    attribute = 'ipacertmapdata'
+    takes_options = (
+        DNParam(
+            'issuer?',
+            cli_name='issuer',
+            label=_('Issuer'),
+            doc=_('Issuer of the certificate'),
+            flags=['virtual_attribute']
+        ),
+        DNParam(
+            'subject?',
+            cli_name='subject',
+            label=_('Subject'),
+            doc=_('Subject of the certificate'),
+            flags=['virtual_attribute']
+        ),
+        Bytes(
+            'certificate*', validate_certificate,
+            cli_name='certificate',
+            label=_('Certificate'),
+            doc=_('Base-64 encoded user certificate'),
+            flags=['virtual_attribute']
+        ),
+    )
+
+    @staticmethod
+    def _build_mapdata(subject, issuer):
+        return u'X509:<I>{issuer}<S>{subject}'.format(
+            issuer=issuer.x500_text(), subject=subject.x500_text())
+
+    @classmethod
+    def _convert_options_to_certmap(cls, entry_attrs, issuer=None,
+                                    subject=None, certificates=()):
+        """
+        Converts options to ipacertmapdata
+
+        When --subject --issuer or --certificate 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 --certificate is used multiple
+        times, or in conjunction with --subject --issuer.
+        """
+        data = []
+        data.extend(entry_attrs.get(cls.attribute, list()))
+
+        if issuer or subject:
+            data.append(cls._build_mapdata(subject, issuer))
+
+        for dercert in certificates:
+            cert = x509.load_certificate(dercert, x509.DER)
+            issuer = DN(cert.issuer)
+            subject = DN(cert.subject)
+            if not subject:
+                raise errors.ValidationError(
+                    name='certificate',
+                    error=_('cannot have an empty subject'))
+            data.append(cls._build_mapdata(subject, issuer))
+
+        entry_attrs[cls.attribute] = data
+
+    def get_args(self):
+        # ipacertmapdata is not mandatory as it can be built
+        # from the values subject+issuer or from reading certificate
+        for arg in super(ModCertMapData, self).get_args():
+            if arg.name == 'ipacertmapdata':
+                yield arg.clone(required=False, alwaysask=False)
+            else:
+                yield arg.clone()
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+                     **options):
+        # The 3 valid calls are
+        # ipa user-add-certmapdata LOGIN --subject xx --issuer yy
+        # ipa user-add-certmapdata LOGIN [DATA] --certificate xx
+        # ipa user-add-certmapdata LOGIN DATA
+        # Check that at least one of the 3 formats is used
+
+        try:
+            certmapdatas = keys[1] or []
+        except IndexError:
+            certmapdatas = []
+        issuer = options.get('issuer')
+        subject = options.get('subject')
+        certificates = options.get('certificate', [])
+
+        # If only LOGIN is supplied, then we need either subject or issuer or
+        # certificate
+        if (not certmapdatas and not issuer and not subject and
+                not certificates):
+            raise errors.RequirementError(name='ipacertmapdata')
+
+        # If subject or issuer is provided, other options are not allowed
+        if subject or issuer:
+            if certificates:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('cannot specify both subject/issuer '
+                             'and certificate'))
+            if certmapdatas:
+                raise errors.MutuallyExclusiveError(
+                    reason=_('cannot specify both subject/issuer '
+                             'and ipacertmapdata'))
+            # If subject or issuer is provided, then the other one is required
+            if not subject:
+                raise errors.RequirementError(name='subject')
+            if not issuer:
+                raise errors.RequirementError(name='issuer')
+
+        # 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')
+
+        self._convert_options_to_certmap(
+            entry_attrs,
+            issuer=issuer,
+            subject=subject,
+            certificates=certificates)
+
+        return dn
+
+
+class baseuser_add_certmapdata(ModCertMapData, LDAPAddAttribute):
+    __doc__ = _("Add one or more certificate mappings to the user entry.")
+    msg_summary = _('Added certificate mappings to user "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+                     **options):
+
+        dn = super(baseuser_add_certmapdata, self).pre_callback(
+            ldap, dn, entry_attrs, attrs_list, *keys, **options)
+
+        # The objectclass ipacertmapobject may not be present on
+        # existing user entries. We need to add it if we define a new
+        # value for ipacertmapdata
+        add_missing_object_class(ldap, u'ipacertmapobject', dn)
+
+        return dn
+
+
+class baseuser_remove_certmapdata(ModCertMapData,
+                                  LDAPRemoveAttribute):
+    __doc__ = _("Remove one or more certificate mappings from the user entry.")
+    msg_summary = _('Removed certificate mappings from user "%(value)s"')
diff --git a/ipaserver/plugins/certmap.py b/ipaserver/plugins/certmap.py
new file mode 100644
index 0000000..c37eae3
--- /dev/null
+++ b/ipaserver/plugins/certmap.py
@@ -0,0 +1,391 @@
+# Authors:
+#   Florence Blanc-Renaud <f...@redhat.com>
+#
+# Copyright (C) 2017  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import six
+
+from ipalib import api, errors
+from ipalib.parameters import Bool, DNSNameParam, Flag, Int, Str
+from ipalib.plugable import Registry
+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 rule1 --desc="Link certificate with subject and issuer"
+""") + _("""
+ Modify a Certificate Identity Mapping Rule:
+   ipa certmaprule-mod rule1 --maprule="<ALT-SEC-ID-I-S:altSecurityIdentities>"
+""") + _("""
+ Disable a Certificate Identity Mapping Rule:
+   ipa certmaprule-disable rule1
+""") + _("""
+ Enable a Certificate Identity Mapping Rule:
+   ipa certmaprule-enable rule1
+""") + _("""
+ Display information about a Certificate Identity Mapping Rule:
+   ipa certmaprule-show rule1
+""") + _("""
+ 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 rule1
+""")
+
+register = Registry()
+
+
+def check_associateddomain_is_trusted(api_inst, options):
+    """
+    Check that the associateddomain in options are either IPA domain or
+    a trusted domain.
+
+    :param api_inst: API instance
+    :param associateddomain: domains to be checked
+
+    :raises: ValidationError if the domain is neither IPA domain nor trusted
+    """
+    domains = options.get('associateddomain')
+    if domains:
+        trust_suffix_namespace = set()
+        trust_suffix_namespace.add(api_inst.env.domain.lower())
+
+        trust_objects = api_inst.Command.trust_find(sizelimit=0)['result']
+        for obj in trust_objects:
+            trustdomains = api_inst.Command.trustdomain_find(
+                obj['cn'][0], sizelimit=0)['result']
+            for domain in trustdomains:
+                trust_suffix_namespace.add(domain['cn'][0].lower())
+
+        for dom in domains:
+            if not str(dom).lower() in trust_suffix_namespace:
+                raise errors.ValidationError(
+                    name=_('domain'),
+                    error=_('The domain %s is neither IPA domain nor a trusted'
+                            'domain.') % dom
+                    )
+
+
+@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',
+        'ipacertmapmaprule',
+        'ipacertmapmatchrule',
+        'associateddomain',
+        'ipacertmappriority',
+        'ipaenabledflag'
+    ]
+    search_attributes = [
+        'cn', 'description',
+        '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'),
+        ),
+        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'),
+        ),
+        DNSNameParam(
+            'associateddomain*',
+            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,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'objectclass', 'cn', 'description',
+                'ipacertmapmaprule', 'ipacertmapmatchrule', 'associateddomain',
+                'ipacertmappriority', 'ipaenabledflag',
+            },
+        },
+        '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',
+                'ipacertmapmaprule', 'ipacertmapmatchrule', 'associateddomain',
+                'ipacertmappriority', 'ipaenabledflag',
+            },
+            'default_privileges': {
+                'Certificate Identity Mapping Administrators'},
+        },
+    }
+
+
+@register()
+class certmaprule_add(LDAPCreate):
+    __doc__ = _('Create a new Certificate Identity Mapping Rule.')
+
+    msg_summary = _('Added Certificate Identity Mapping Rule "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+                     **options):
+        check_associateddomain_is_trusted(self.api, options)
+        return dn
+
+
+@register()
+class certmaprule_mod(LDAPUpdate):
+    __doc__ = _('Modify a Certificate Identity Mapping Rule.')
+
+    msg_summary = _('Modified Certificate Identity Mapping Rule "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+                     **options):
+        check_associateddomain_is_trusted(self.api, options)
+        return dn
+
+
+@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/stageuser.py b/ipaserver/plugins/stageuser.py
index 5602514..c7ea478 100644
--- a/ipaserver/plugins/stageuser.py
+++ b/ipaserver/plugins/stageuser.py
@@ -44,7 +44,9 @@
     baseuser_add_principal,
     baseuser_remove_principal,
     baseuser_add_manager,
-    baseuser_remove_manager)
+    baseuser_remove_manager,
+    baseuser_add_certmapdata,
+    baseuser_remove_certmapdata)
 from ipalib.request import context
 from ipalib.util import set_krbcanonicalname
 from ipalib import _, ngettext
@@ -772,3 +774,15 @@ class stageuser_add_principal(baseuser_add_principal):
 class stageuser_remove_principal(baseuser_remove_principal):
     __doc__ = _('Remove principal alias from the stageuser entry')
     msg_summary = _('Removed aliases from stageuser "%(value)s"')
+
+
+@register()
+class stageuser_add_certmapdata(baseuser_add_certmapdata):
+    __doc__ = _("Add one or more certificate mappings to the stage user"
+                " entry.")
+
+
+@register()
+class stageuser_remove_certmapdata(baseuser_remove_certmapdata):
+    __doc__ = _("Remove one or more certificate mappings from the stage user"
+                " entry.")
diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py
index 88171cf..2d29dfb 100644
--- a/ipaserver/plugins/user.py
+++ b/ipaserver/plugins/user.py
@@ -22,7 +22,6 @@
 from time import gmtime, strftime
 import posixpath
 import os
-
 import six
 
 from ipalib import api
@@ -46,7 +45,9 @@
     baseuser_add_cert,
     baseuser_remove_cert,
     baseuser_add_principal,
-    baseuser_remove_principal)
+    baseuser_remove_principal,
+    baseuser_add_certmapdata,
+    baseuser_remove_certmapdata)
 from .idviews import remove_ipaobject_overrides
 from ipalib.plugable import Registry
 from .baseldap import (
@@ -179,6 +180,7 @@ class user(baseuser):
                 'secretary', 'usercertificate',
                 'usersmimecertificate', 'x500uniqueidentifier',
                 'inetuserhttpurl', 'inetuserstatus',
+                'ipacertmapdata',
             },
             'fixup_function': fix_addressbook_permission_bindrule,
         },
@@ -366,6 +368,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 + (
@@ -1185,6 +1194,16 @@ class user_remove_cert(baseuser_remove_cert):
 
 
 @register()
+class user_add_certmapdata(baseuser_add_certmapdata):
+    __doc__ = _("Add one or more certificate mappings to the user entry.")
+
+
+@register()
+class user_remove_certmapdata(baseuser_remove_certmapdata):
+    __doc__ = _("Remove one or more certificate mappings from the user entry.")
+
+
+@register()
 class user_add_manager(baseuser_add_manager):
     __doc__ = _("Add a manager to the user entry")
 
diff --git a/ipatests/test_ipapython/test_dn.py b/ipatests/test_ipapython/test_dn.py
index 3ca3b57..24b6093 100644
--- a/ipatests/test_ipapython/test_dn.py
+++ b/ipatests/test_ipapython/test_dn.py
@@ -1184,6 +1184,26 @@ def test_hashing(self):
         self.assertFalse(dn3_a in s)
         self.assertFalse(dn3_b in s)
 
+    def test_x500_text(self):
+        # null DN x500 ordering and LDAP ordering are the same
+        nulldn = DN()
+        self.assertEqual(nulldn.ldap_text(), nulldn.x500_text())
+
+        # reverse a DN with a single RDN
+        self.assertEqual(self.dn1.ldap_text(), self.dn1.x500_text())
+
+        # reverse a DN with 2 RDNs
+        dn3_x500 = self.dn3.x500_text()
+        dn3_rev = DN(self.rdn2, self.rdn1)
+        self.assertEqual(dn3_rev.ldap_text(), dn3_x500)
+
+        # reverse a longer DN
+        longdn_x500 = self.base_container_dn.x500_text()
+        longdn_rev = DN(longdn_x500)
+        l = len(self.base_container_dn)
+        for i in range(l):
+            self.assertEquals(longdn_rev[i], self.base_container_dn[l-1-i])
+
 
 class TestEscapes(unittest.TestCase):
     def setUp(self):

From 5b6fd5e1709912986ee1bbe0bc2d426a191ea2f0 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <f...@redhat.com>
Date: Thu, 23 Feb 2017 18:04:47 +0100
Subject: [PATCH 2/2] IdM Server: list all Employees with matching Smart Card

Implement a new IPA command allowing to retrieve the list of users matching
the provided certificate.
The command is using SSSD Dbus interface, thus including users from IPA
domain and from trusted domains. This requires sssd-dbus package to be
installed on IPA server.

https://fedorahosted.org/freeipa/ticket/6646
---
 API.txt                      |  12 ++++
 freeipa.spec.in              |   2 +
 ipaserver/plugins/certmap.py | 153 ++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 166 insertions(+), 1 deletion(-)

diff --git a/API.txt b/API.txt
index a8f8ff1..ace3101 100644
--- a/API.txt
+++ b/API.txt
@@ -824,6 +824,16 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: certmap_match/1
+args: 1,3,4
+arg: Bytes('certificate', cli_name='certificate')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=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: certmapconfig_mod/1
 args: 0,8,3
 option: Str('addattr*', cli_name='addattr')
@@ -6517,6 +6527,8 @@ default: cert_request/1
 default: cert_revoke/1
 default: cert_show/1
 default: cert_status/1
+default: certmap/1
+default: certmap_match/1
 default: certmapconfig/1
 default: certmapconfig_mod/1
 default: certmapconfig_show/1
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 5c835ca..5e66a2f 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -269,6 +269,8 @@ Requires: gzip
 Requires: oddjob
 # Require 0.6.0 for the new delegation access control features
 Requires: gssproxy >= 0.6.0
+# Require 1.15.1 for the certificate identity mapping feature
+Requires: sssd-dbus >= 1.15.1
 
 Provides: %{alt_name}-server = %{version}
 Conflicts: %{alt_name}-server
diff --git a/ipaserver/plugins/certmap.py b/ipaserver/plugins/certmap.py
index c37eae3..09f0519 100644
--- a/ipaserver/plugins/certmap.py
+++ b/ipaserver/plugins/certmap.py
@@ -17,9 +17,14 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import base64
+import dbus
 import six
 
-from ipalib import api, errors
+from ipalib import api, errors, x509
+from ipalib import Bytes
+from ipalib.crud import Search
+from ipalib.frontend import Object
 from ipalib.parameters import Bool, DNSNameParam, Flag, Int, Str
 from ipalib.plugable import Registry
 from .baseldap import (
@@ -33,6 +38,7 @@
     pkey_to_value)
 from ipalib import _, ngettext
 from ipalib import output
+from ipaserver.plugins.service import validate_certificate
 
 
 if six.PY3:
@@ -389,3 +395,148 @@ def execute(self, cn, **options):
             result=True,
             value=pkey_to_value(cn, options),
         )
+
+
+DBUS_SSSD_NAME = 'org.freedesktop.sssd.infopipe'
+DBUS_PROPERTY_IF = 'org.freedesktop.DBus.Properties'
+DBUS_SSSD_USERS_PATH = '/org/freedesktop/sssd/infopipe/Users'
+DBUS_SSSD_USERS_IF = 'org.freedesktop.sssd.infopipe.Users'
+DBUS_SSSD_USER_IF = 'org.freedesktop.sssd.infopipe.Users.User'
+
+
+class _sssd(object):
+    """
+    Auxiliary class for SSSD infopipe DBus.
+    """
+    def __init__(self, log):
+        """
+        Initialize the Users object and interface.
+
+       :raise RemoteRetrieveError: if DBus error occurs
+        """
+        try:
+            self.log = log
+            self._bus = dbus.SystemBus()
+            self._users_obj = self._bus.get_object(
+                DBUS_SSSD_NAME, DBUS_SSSD_USERS_PATH)
+            self._users_iface = dbus.Interface(
+                self._users_obj, DBUS_SSSD_USERS_IF)
+        except dbus.DBusException as e:
+            self.log.error(
+                'Failed to initialize DBus interface {iface}. DBus '
+                'exception is {exc}.'.format(iface=DBUS_SSSD_USERS_IF, exc=e)
+                )
+            raise errors.RemoteRetrieveError(
+                reason=_('Failed to connect to sssd over SystemBus. '
+                         'See details in the error_log'))
+
+    def list_users_by_cert(self, cert):
+        """
+        Look for users matching the cert.
+
+        Call Users.ListByCertificate interface and return a dict
+        with key = domain, value = list of uids
+        corresponding to the users matching the provided cert
+        :param cert: DER cert
+        :raise RemoteRetrieveError: if DBus error occurs
+        """
+        try:
+            pem = x509.make_pem(base64.b64encode(cert))
+            # bug 3306 in sssd returns 0 entry when max_entries = 0
+            # Temp workaround is to use a non-null value, not too high
+            # to avoid reserving unneeded memory
+            max_entries = dbus.UInt32(100)
+            user_paths = self._users_iface.ListByCertificate(pem, max_entries)
+            users = dict()
+            for user_path in user_paths:
+                user_obj = self._bus.get_object(DBUS_SSSD_NAME, user_path)
+                user_iface = dbus.Interface(user_obj, DBUS_PROPERTY_IF)
+                user_login = user_iface.Get(DBUS_SSSD_USER_IF, 'name')
+
+                # Extract name@domain
+                items = user_login.split('@')
+                domain = api.env.realm if len(items) < 2 else items[1]
+                name = items[0]
+
+                # Retrieve the list of users for the given domain,
+                # or initialize to an empty list
+                # and add the name
+                users_for_dom = users.setdefault(domain, list())
+                users_for_dom.append(name)
+            return users
+        except dbus.DBusException as e:
+            err_name = e.get_dbus_name()
+            # If there is no matching user, do not consider this as an
+            # exception and return an empty list
+            if err_name == 'org.freedesktop.sssd.Error.NotFound':
+                return dict()
+            self.log.error(
+                'Failed to use interface {iface}. DBus '
+                'exception is {exc}.'.format(iface=DBUS_SSSD_USERS_IF, exc=e))
+            raise errors.RemoteRetrieveError(
+                reason=_('Failed to find users over SystemBus. '
+                         ' See details in the error_log'))
+
+
+@register()
+class certmap(Object):
+    """
+    virtual object for certmatch_map API
+    """
+    takes_params = (
+        DNSNameParam(
+            'domain',
+            label=_('Domain'),
+            flags={'no_search'},
+        ),
+        Str(
+            'uid*',
+            label=_('Usernames'),
+            flags={'no_search'},
+        ),
+    )
+
+
+@register()
+class certmap_match(Search):
+    __doc__ = _('Search for users matching the provided certificate.')
+
+    msg_summary = ngettext('%(count)s user matched',
+                           '%(count)s users matched', 0)
+
+    def get_args(self):
+        for arg in super(certmap_match, self).get_args():
+            if arg.name == 'criteria':
+                continue
+            yield arg
+        yield Bytes(
+            'certificate', validate_certificate,
+            cli_name='certificate',
+            label=_('Certificate'),
+            doc=_('Base-64 encoded user certificate'),
+            flags=['virtual_attribute']
+        )
+
+    def execute(self, *args, **options):
+        """
+        Search for users matching the provided certificate.
+
+        The search is performed using SSSD's DBus interface
+        Users.ListByCertificate.
+        SSSD does the lookup based on certificate mapping rules, using
+        FreeIPA domain and trusted domains.
+        :raise RemoteRetrieveError: if DBus returns an exception
+        """
+        sssd = _sssd(self.log)
+
+        cert = args[0]
+        users = sssd.list_users_by_cert(cert)
+        count = sum([len(l) for (_k, l) in users.items()])
+        result = [{'domain': domain, 'uid': userlist}
+                  for (domain, userlist) in users.items()]
+
+        return dict(
+            result=result,
+            count=count,
+            truncated=False,
+        )
-- 
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