Hi all,

I think the automatic CSR generation feature (https://fedorahosted.org/freeipa/ticket/4899, http://www.freeipa.org/page/V4/Automatic_Certificate_Request_Generation) is stable enough to review now. The following are summaries of the attached patches:
0004: LDAP schema changes for the new feature
0005: Basic API for new objects and CSR generation
0006: Update install automation to create some default mapping rules
0007: Implement the lookups and text processing that generates the CSR config 0008 and 0009: Implement some actual transformation rules so that the feature is usable
0010: Add a new cert profile for user certs, with mappings
0011: Implement import/export of cert profiles with mappings
0012: Tests for profile import/export

Generally speaking, later patches depend on earlier ones, but I don't anticipate any problems from committing earlier patches without later ones.

If you prefer, you can also comment on the pull request version: https://github.com/LiptonB/freeipa/pull/4. Note that I may force push on this branch.

Allocation of OIDs for schema change also needs review: https://code.engineering.redhat.com/gerrit/#/c/80061/

Known issues:
- When the requested principal does not have some of the requested data, produces funny-looking configs with extra commas, empty sections, etc. They are still accepted by my copies of openssl and certutil, but they look ugly. - The new objects don't have any ACIs, so for the moment only admin can run the new commands. - Does not yet have support for prompting user for field values, so currently all data must come from the database. - All processing happens on the server side. As discussed in a previous thread, it would be desirable to break this out into a library so it could be used client-side.

Very excited to hear your thoughts!
Ben
From b2b9d3acd4eb7529f7d6ca5d58ddff546481fdf0 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Tue, 5 Jul 2016 14:19:35 -0400
Subject: [PATCH 1/9] Add schema to support automatic CSR generation

This adds the schema discussed in
http://www.freeipa.org/page/V4/Automatic_Certificate_Request_Generation/Schema#Option_A, along with containers for the new ipaCertMappingRuleset objectClass. There are no containers for ipaCertFieldMappingRule and ipaCertTransformationRule because they will be stored as child objects of ipaCertProfile and ipaCertMappingRuleset objects respectively.

https://fedorahosted.org/freeipa/ticket/4899
---
 install/share/60certificate-profiles.ldif | 7 +++++++
 install/share/bootstrap-template.ldif     | 6 ++++++
 install/updates/41-cert-mapping.update    | 4 ++++
 install/updates/Makefile.am               | 1 +
 ipalib/constants.py                       | 2 ++
 5 files changed, 20 insertions(+)
 create mode 100644 install/updates/41-cert-mapping.update

diff --git a/install/share/60certificate-profiles.ldif b/install/share/60certificate-profiles.ldif
index a87fe667d56768419dacf57103e347e88c945e2a..031cec05fc5b19b38b568c10eaf2120d58326396 100644
--- a/install/share/60certificate-profiles.ldif
+++ b/install/share/60certificate-profiles.ldif
@@ -7,6 +7,13 @@ attributeTypes: (2.16.840.1.113730.3.8.21.1.5 NAME 'ipaCertProfileCategory' DESC
 attributeTypes: (2.16.840.1.113730.3.8.21.1.6 NAME 'ipaCaId' DESC 'Dogtag Authority ID' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.4 Lightweight CAs' )
 attributeTypes: (2.16.840.1.113730.3.8.21.1.7 NAME 'ipaCaIssuerDN' DESC 'Issuer DN' SUP distinguishedName X-ORIGIN 'IPA v4.4 Lightweight CAs' )
 attributeTypes: (2.16.840.1.113730.3.8.21.1.8 NAME 'ipaCaSubjectDN' DESC 'Subject DN' SUP distinguishedName X-ORIGIN 'IPA v4.4 Lightweight CAs' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.9 NAME 'ipaCertSyntaxMapping' DESC 'Reference to ipaCertMappingRuleset: How to format the specification for this field' SUP distinguishedName EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.5' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.10 NAME 'ipaCertDataMapping' DESC 'Reference to ipaCertMappingRuleset: How to map data into field values' SUP distinguishedName EQUALITY distinguishedNameMatch 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.21.1.11 NAME 'ipaCertTransformationTemplate' DESC 'How to transform a specific data item' EQUALITY caseExactMatch 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.21.1.12 NAME 'ipaCertTransformationHelper' DESC 'Helper to which this transformation is targeted' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.5' )
 objectClasses: (2.16.840.1.113730.3.8.21.2.1 NAME 'ipaCertProfile' SUP top STRUCTURAL MUST ( cn $ description $ ipaCertProfileStoreIssued ) X-ORIGIN 'IPA v4.2' )
 objectClasses: (2.16.840.1.113730.3.8.21.2.2 NAME 'ipaCaAcl' SUP ipaAssociation STRUCTURAL MUST cn MAY ( ipaCaCategory $ ipaCertProfileCategory $ userCategory $ hostCategory $ serviceCategory $ ipaMemberCa $ ipaMemberCertProfile $ memberService ) X-ORIGIN 'IPA v4.2' )
 objectClasses: (2.16.840.1.113730.3.8.21.2.3 NAME 'ipaCa' SUP top STRUCTURAL MUST ( cn $ ipaCaId $ ipaCaSubjectDN $ ipaCaIssuerDN ) MAY description X-ORIGIN 'IPA v4.4 Lightweight CAs' )
+objectClasses: (2.16.840.1.113730.3.8.21.2.4 NAME 'ipaCertFieldMappingRule' SUP top STRUCTURAL MUST ( cn $ ipaCertSyntaxMapping $ ipaCertDataMapping ) X-ORIGIN 'IPA v4.5' )
+objectClasses: (2.16.840.1.113730.3.8.21.2.5 NAME 'ipaCertMappingRuleset' SUP top STRUCTURAL MUST ( cn $ description ) X-ORIGIN 'IPA v4.5' )
+objectClasses: (2.16.840.1.113730.3.8.21.2.6 NAME 'ipaCertTransformationRule' SUP top STRUCTURAL MUST ( cn $ ipaCertTransformationTemplate $ ipaCertTransformationHelper ) X-ORIGIN 'IPA v4.5' )
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
index da12ddf0ca887e8305402048ceed5d5b28816164..ccd414651627dbfd3f91cde1b4ddd23876ca56cc 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -482,3 +482,9 @@ changetype: add
 objectClass: nsContainer
 objectClass: top
 cn: cas
+
+dn: cn=mappingrulesets,cn=ca,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: mappingrulesets
diff --git a/install/updates/41-cert-mapping.update b/install/updates/41-cert-mapping.update
new file mode 100644
index 0000000000000000000000000000000000000000..c161a9c7b2417b5278fb09372992db4f0649b55d
--- /dev/null
+++ b/install/updates/41-cert-mapping.update
@@ -0,0 +1,4 @@
+dn: cn=mappingrulesets,cn=ca,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: mappingrulesets
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 455fd209d171888dc94a7f708dc5fa1743f62bf4..e1be363b077a9e35cf13822eb10dc92245410ada 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -40,6 +40,7 @@ app_DATA =				\
 	40-vault.update			\
 	41-caacl.update			\
 	41-lightweight-cas.update	\
+	41-cert-mapping.update		\
 	45-roles.update			\
 	50-7_bit_check.update	        \
 	50-dogtag10-migration.update	\
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 0574bb3aa457dd79a6d64f6b8a6b57161d32da92..1f286a1e021a758410dc2b75b0dfbd5b60f76cb9 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -124,6 +124,8 @@ DEFAULT_CONFIG = (
     ('container_locations', DN(('cn', 'locations'), ('cn', 'etc'))),
     ('container_ca', DN(('cn', 'cas'), ('cn', 'ca'))),
     ('container_dnsservers', DN(('cn', 'servers'), ('cn', 'dns'))),
+    ('container_certmappingruleset',
+        DN(('cn', 'mappingrulesets'), ('cn', 'ca'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
-- 
2.5.5

From 9c49ec75ec21270e2c63b7a10709b1f022ed53d1 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Tue, 5 Jul 2016 14:19:49 -0400
Subject: [PATCH 2/9] Add plugin for CSR generation

This plugin will implement the cert-get-requestdata call that returns a
config that can be used to generate a CSR. This commit implements only
the very basic API of the plugin. Actual functionality will come in
later patches.

https://fedorahosted.org/freeipa/ticket/4899
---
 API.txt                          | 202 ++++++++++++++++++++++++++++++
 VERSION                          |   4 +-
 ipaclient/plugins/certmapping.py |  27 ++++
 ipaserver/plugins/certmapping.py | 262 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 493 insertions(+), 2 deletions(-)
 create mode 100644 ipaclient/plugins/certmapping.py
 create mode 100644 ipaserver/plugins/certmapping.py

diff --git a/API.txt b/API.txt
index 535d8ec9a4990395207e2455a09a8c1bdef5529a..f317e5847cdbe881ac99caaa69882786fdd7cbdb 100644
--- a/API.txt
+++ b/API.txt
@@ -759,6 +759,13 @@ output: Output('count', type=[<type 'int'>])
 output: ListOfEntries('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: Output('truncated', type=[<type 'bool'>])
+command: cert_get_requestdata/1
+args: 0,4,1
+option: Str('format')
+option: Principal('principal')
+option: Str('profile_id?')
+option: Str('version?')
+output: Output('result', type=[<type 'dict'>])
 command: cert_remove_hold/1
 args: 1,2,1
 arg: Int('serial_number')
@@ -808,6 +815,116 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: certfieldmappingrule_add/1
+args: 2,7,3
+arg: Str('certprofilecn', cli_name='certprofile')
+arg: Str('cn', cli_name='id')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: DNParam('ipacertdatamapping+', cli_name='datarule')
+option: DNParam('ipacertsyntaxmapping', cli_name='syntaxrule')
+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: certfieldmappingrule_del/1
+args: 2,2,3
+arg: Str('certprofilecn', cli_name='certprofile')
+arg: Str('cn+', cli_name='id')
+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: certfieldmappingrule_find/1
+args: 2,9,4
+arg: Str('certprofilecn', cli_name='certprofile')
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('cn?', autofill=False, cli_name='id')
+option: DNParam('ipacertdatamapping*', autofill=False, cli_name='datarule')
+option: DNParam('ipacertsyntaxmapping?', autofill=False, cli_name='syntaxrule')
+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: certfieldmappingrule_show/1
+args: 2,4,3
+arg: Str('certprofilecn', cli_name='certprofile')
+arg: Str('cn', cli_name='id')
+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: certmappingrule_add/1
+args: 1,6,3
+arg: Str('cn', cli_name='id')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('description', cli_name='description')
+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: certmappingrule_del/1
+args: 1,2,3
+arg: Str('cn+', cli_name='id')
+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: certmappingrule_find/1
+args: 1,8,4
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('cn?', autofill=False, cli_name='id')
+option: Str('description?', autofill=False, cli_name='description')
+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: certmappingrule_mod/1
+args: 1,8,3
+arg: Str('cn', cli_name='id')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('delattr*', cli_name='delattr')
+option: Str('description?', autofill=False, cli_name='description')
+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: certmappingrule_show/1
+args: 1,4,3
+arg: Str('cn', cli_name='id')
+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')
@@ -871,6 +988,73 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: certtransformationrule_add/1
+args: 2,7,3
+arg: Str('certmappingrulecn', cli_name='certmappingrule')
+arg: Str('cn', cli_name='id')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('ipacerttransformationhelper+', cli_name='helper')
+option: Str('ipacerttransformationtemplate', cli_name='template')
+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: certtransformationrule_del/1
+args: 2,2,3
+arg: Str('certmappingrulecn', cli_name='certmappingrule')
+arg: Str('cn+', cli_name='id')
+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: certtransformationrule_find/1
+args: 2,9,4
+arg: Str('certmappingrulecn', cli_name='certmappingrule')
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('cn?', autofill=False, cli_name='id')
+option: Str('ipacerttransformationhelper*', autofill=False, cli_name='helper')
+option: Str('ipacerttransformationtemplate?', autofill=False, cli_name='template')
+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: certtransformationrule_mod/1
+args: 2,9,3
+arg: Str('certmappingrulecn', cli_name='certmappingrule')
+arg: Str('cn', cli_name='id')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('delattr*', cli_name='delattr')
+option: Str('ipacerttransformationhelper*', autofill=False, cli_name='helper')
+option: Str('ipacerttransformationtemplate?', autofill=False, cli_name='template')
+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: certtransformationrule_show/1
+args: 2,4,3
+arg: Str('certmappingrulecn', cli_name='certmappingrule')
+arg: Str('cn', cli_name='id')
+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: class_find/1
 args: 1,4,4
 arg: Str('criteria?')
@@ -6269,11 +6453,23 @@ default: caacl_remove_user/1
 default: caacl_show/1
 default: cert/1
 default: cert_find/1
+default: cert_get_requestdata/1
 default: cert_remove_hold/1
 default: cert_request/1
 default: cert_revoke/1
 default: cert_show/1
 default: cert_status/1
+default: certfieldmappingrule/1
+default: certfieldmappingrule_add/1
+default: certfieldmappingrule_del/1
+default: certfieldmappingrule_find/1
+default: certfieldmappingrule_show/1
+default: certmappingrule/1
+default: certmappingrule_add/1
+default: certmappingrule_del/1
+default: certmappingrule_find/1
+default: certmappingrule_mod/1
+default: certmappingrule_show/1
 default: certprofile/1
 default: certprofile_del/1
 default: certprofile_find/1
@@ -6281,6 +6477,12 @@ default: certprofile_import/1
 default: certprofile_mod/1
 default: certprofile_show/1
 default: certreq/1
+default: certtransformationrule/1
+default: certtransformationrule_add/1
+default: certtransformationrule_del/1
+default: certtransformationrule_find/1
+default: certtransformationrule_mod/1
+default: certtransformationrule_show/1
 default: class/1
 default: class_find/1
 default: class_show/1
diff --git a/VERSION b/VERSION
index ca489965050f32d2d8987dfd251ec2b2a0ba1768..17a8aeb2253bdf603a1f38bb1133cb3187f3d97b 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=211
-# Last change: mbabinsk: allow 'value' output param in commands without primary key
+IPA_API_VERSION_MINOR=212
+# Last change: blipton - Add plugin for CSR generation
diff --git a/ipaclient/plugins/certmapping.py b/ipaclient/plugins/certmapping.py
new file mode 100644
index 0000000000000000000000000000000000000000..818bd940bbb979b21d7e804c23867bf8c5d16438
--- /dev/null
+++ b/ipaclient/plugins/certmapping.py
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+from ipaclient.frontend import CommandOverride, Str
+from ipalib.plugable import Registry
+from ipalib.text import _
+
+register = Registry()
+
+import six
+
+if six.PY3:
+    unicode = str
+
+__doc__ = _("""
+Temporary command override to display debug data generated by
+the server-side plugin
+""")
+
+@register(override=True, no_fail=True)
+class cert_get_requestdata(CommandOverride):
+    has_output_params = (
+        Str('debug_output',
+            label=_('Debug output'),
+        ),
+    )
diff --git a/ipaserver/plugins/certmapping.py b/ipaserver/plugins/certmapping.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5dc33658ce49a62ad10092f6c9588ab21e270c0
--- /dev/null
+++ b/ipaserver/plugins/certmapping.py
@@ -0,0 +1,262 @@
+
+from ipalib import api
+from ipalib import DNParam, Str, Command
+from ipalib import output
+from ipalib.parameters import Principal
+from ipalib.plugable import Registry
+from ipalib.text import _
+from .baseldap import (
+    LDAPCreate, LDAPObject, LDAPRetrieve, LDAPSearch, LDAPUpdate, LDAPDelete)
+from .certprofile import validate_profile_id
+
+
+__doc__ = _("""
+Mappings from FreeIPA data to Certificate Signing Requests.
+""")
+
+
+register = Registry()
+
+
+@register()
+class certfieldmappingrule(LDAPObject):
+    """
+    Certificate Field Mapping Rule object. Specifies how a particular cert
+    field should be constructed within this profile.
+    """
+    parent_object = 'certprofile'
+
+    object_name = _('Certificate Field Mapping Rule')
+    object_name_plural = _('Certificate Field Mapping Rules')
+    object_class = ['ipacertfieldmappingrule']
+    default_attributes = [
+        'cn', 'ipacertsyntaxmapping', 'ipacertdatamapping'
+    ]
+    search_attributes = [
+        'cn', 'ipacertsyntaxmapping', 'ipacertdatamapping'
+    ]
+    label = _('Certificate Field Mapping Rules')
+    label_singular = _('Certificate Field Mapping Rule')
+
+    takes_params = (
+        Str('cn',
+            primary_key=True,
+            cli_name='id',
+            label=_('Field Mapping Rule ID'),
+            doc=_('ID for referring to this field mapping rule'),
+        ),
+        DNParam('ipacertsyntaxmapping',
+            required=True,
+            cli_name='syntaxrule',
+            label=_('Mapping ruleset for field syntax'),
+            doc=_('Mapping ruleset for formatting entire field'),
+        ),
+        DNParam('ipacertdatamapping',
+            required=True,
+            multivalue=True,
+            cli_name='datarule',
+            label=_('Mapping ruleset for data items'),
+            doc=_('Mapping ruleset for formatting individual items of data'),
+        ),
+    )
+
+
+@register()
+class certfieldmappingrule_add(LDAPCreate):
+    NO_CLI = True
+
+    __doc__ = _("""Create a new Cert Field Mapping Rule""")
+
+
+@register()
+class certfieldmappingrule_find(LDAPSearch):
+    NO_CLI = True
+
+    __doc__ = _("""Search for Cert Field Mapping Rules""")
+
+
+@register()
+class certfieldmappingrule_show(LDAPRetrieve):
+    NO_CLI = True
+
+    __doc__ = _("""Retrieve a Cert Field Mapping Rule""")
+
+
+@register()
+class certfieldmappingrule_del(LDAPDelete):
+    NO_CLI = True
+
+    __doc__ = _("""Delete a Cert Field Mapping Rule""")
+
+
+@register()
+class certmappingrule(LDAPObject):
+    """
+    Certificate Mapping Rule object. Specifies how a particular cert
+    field should be constructed within this profile.
+    """
+    container_dn = api.env.container_certmappingruleset
+    object_name = _('Certificate Mapping Rule')
+    object_name_plural = _('Certificate Mapping Rules')
+    object_class = ['ipacertmappingruleset']
+    default_attributes = [
+        'cn', 'description'
+    ]
+    search_attributes = [
+        'cn', 'description'
+    ]
+    label = _('Certificate Mapping Rules')
+    label_singular = _('Certificate Mapping Rule')
+
+    takes_params = (
+        Str('cn',
+            primary_key=True,
+            cli_name='id',
+            label=_('Field Mapping Rule ID'),
+            doc=_('ID for referring to this mapping rule'),
+        ),
+        Str('description',
+            required=True,
+            cli_name='description',
+            label=_('Description of this mapping rule'),
+            doc=_('Description of this mapping rule'),
+        ),
+    )
+
+
+@register()
+class certmappingrule_add(LDAPCreate):
+    __doc__ = _("""Create a new Certificate Mapping Rule""")
+
+
+@register()
+class certmappingrule_mod(LDAPUpdate):
+    __doc__ = _("""Update a Certificate Mapping Rule""")
+
+
+@register()
+class certmappingrule_find(LDAPSearch):
+    __doc__ = _("""Search for Certificate Mapping Rules""")
+
+
+@register()
+class certmappingrule_show(LDAPRetrieve):
+    __doc__ = _("""Retrieve a Certificate Mapping Rule""")
+
+
+@register()
+class certmappingrule_del(LDAPDelete):
+    __doc__ = _("""Delete a Certificate Mapping Rule""")
+
+
+@register()
+class certtransformationrule(LDAPObject):
+    """
+    Certificate Transformation rule object. Specifies a particular data
+    transformation (comparable to a format string) that is used in converting
+    stored data to certificate requests.
+    """
+    parent_object = 'certmappingrule'
+
+    object_name = _('Certificate Transformation Rule')
+    object_name_plural = _('Certificate Transformation Rules')
+    object_class = ['ipacerttransformationrule']
+    default_attributes = [
+        'cn', 'ipacerttransformationtemplate', 'ipacerttransformationhelper'
+    ]
+    search_attributes = [
+        'cn', 'ipacerttransformationtemplate', 'ipacerttransformationhelper'
+    ]
+    label = _('Certificate Transformation Rules')
+    label_singular = _('Certificate Transformation Rule')
+
+    takes_params = (
+        Str('cn',
+            primary_key=True,
+            cli_name='id',
+            label=_('Field Mapping Rule ID'),
+            doc=_('ID for referring to this transformation rule'),
+        ),
+        Str('ipacerttransformationtemplate',
+            required=True,
+            cli_name='template',
+            label=_('String defining the transformation'),
+            doc=_('String that specifies how the input data should be'
+                  ' formatted and combined'),
+        ),
+        Str('ipacerttransformationhelper',
+            required=True,
+            multivalue=True,
+            cli_name='helper',
+            label=_('Name of CSR generation helper'),
+            doc=_('Name of the CSR generation helper to which the syntax of'
+                  ' this rule is targeted'),
+        ),
+    )
+
+
+@register()
+class certtransformationrule_add(LDAPCreate):
+    __doc__ = _("""Create a new Certificate Transformation Rule""")
+
+
+@register()
+class certtransformationrule_mod(LDAPUpdate):
+    __doc__ = _("""Update a Certificate Transformation Rule""")
+
+
+@register()
+class certtransformationrule_find(LDAPSearch):
+    __doc__ = _("""Search for Certificate Transformation Rules""")
+
+
+@register()
+class certtransformationrule_show(LDAPRetrieve):
+    __doc__ = _("""Retrieve a Certificate Transformation Rule""")
+
+
+@register()
+class certtransformationrule_del(LDAPDelete):
+    __doc__ = _("""Delete a Certificate Transformation Rule""")
+
+
+@register()
+class cert_get_requestdata(Command):
+    __doc__ = _('Gather data for a certificate signing request.')
+
+    takes_options = (
+        Principal('principal',
+            label=_('Principal'),
+            doc=_('Principal for this certificate (e.g.'
+                  ' HTTP/test.example.com)'),
+        ),
+        Str('profile_id?',
+            validate_profile_id,
+            label=_('Profile ID'),
+            doc=_('Certificate Profile to use. If not specified, uses the CA'
+                  ' default.'),
+        ),
+        Str('format',
+            label=_('Name of CSR generation tool'),
+            doc=_('Name of tool (e.g. openssl, certutil) that will be used to'
+                  ' create CSR'),
+        ),
+    )
+
+    has_output = (
+        output.Output(
+            'result',
+            type=dict,
+            doc=_('Dictionary mapping variable name to value'),
+        ),
+    )
+
+    def execute(self, **kw):
+        principal = kw.get('principal')
+        profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE)
+        helper = kw.get('format')
+
+        result = {'debug_output': u'test'}
+        return dict(
+            result=result
+        )
-- 
2.5.5

From 4ddcad401f345f843f99d227eed9dcaa4b33a36a Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Tue, 5 Jul 2016 14:19:55 -0400
Subject: [PATCH 3/9] Add generation rules to the default cert profile

This updates the automation that happens on upgrade to create all the
various types of mapping rules, and adds some default rules to the
caIPAserviceCert profile.

https://fedorahosted.org/freeipa/ticket/4899
---
 ipapython/dogtag.py             | 57 ++++++++++++++++++++++++++----
 ipaserver/install/cainstance.py | 78 ++++++++++++++++++++++++++++++++---------
 2 files changed, 113 insertions(+), 22 deletions(-)

diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index 6f13880026e9e6043649405245c9cd50a826f652..8bd53b0af2e359563778e0c3c868a621c30dda0c 100644
--- a/ipapython/dogtag.py
+++ b/ipapython/dogtag.py
@@ -40,12 +40,57 @@ except ImportError:
 if six.PY3:
     unicode = str
 
-Profile = collections.namedtuple('Profile', ['profile_id', 'description', 'store_issued'])
+Profile = collections.namedtuple('Profile', [
+    'profile_id', 'description', 'store_issued', 'field_mappings'])
+FieldMapping = collections.namedtuple('FieldMapping', [
+    'syntax_mapping', 'data_mappings'])
+MappingRuleset = collections.namedtuple('MappingRuleset', [
+    'id', 'description', 'transformations'])
+TransformationRule = collections.namedtuple('TransformationRule', [
+    'id', 'template', 'helpers'])
 
-INCLUDED_PROFILES = {
-    Profile(u'caIPAserviceCert', u'Standard profile for network services', True),
-    Profile(u'IECUserRoles', u'User profile that includes IECUserRoles extension from request', True),
-    }
+INCLUDED_PROFILES = (
+    Profile(
+        u'caIPAserviceCert', u'Standard profile for network services', True,
+        [
+            FieldMapping(u'syntaxSubject', [u'dataHostCN']),
+            FieldMapping(u'syntaxSAN', [u'dataDNS']),
+        ]),
+    Profile(
+        u'IECUserRoles',
+        u'User profile that includes IECUserRoles extension from request',
+        True, []),
+)
+
+# TODO(blipton): Use the json file import method instead
+INCLUDED_MAPPING_RULESETS = (
+    MappingRuleset(
+        u'syntaxSubject', u'Syntax for adding a Subject Distinguished Name',
+        [
+            TransformationRule(u'syntaxSubjectOpenssl', u'one', [u'openssl']),
+            TransformationRule(
+                u'syntaxSubjectCertutil', u'two', [u'certutil']),
+        ]),
+    MappingRuleset(
+        u'dataHostCN', u'DN with the principal\'s hostname as the CommonName',
+        [
+            TransformationRule(u'dataHostOpenssl', u'three', [u'openssl']),
+            TransformationRule(u'dataHostCertutil', u'four', [u'certutil']),
+        ]),
+    MappingRuleset(
+        u'syntaxSAN', u'Syntax for adding a Subject Alternate Name',
+        [
+            TransformationRule(u'syntaxSANOpenssl', u'five', [u'openssl']),
+            TransformationRule(u'syntaxSANCertutil', u'six', [u'certutil']),
+        ]),
+    MappingRuleset(
+        u'dataDNS',
+        u'Constructs a SubjectAltName entry from the principal\'s hostname',
+        [
+            TransformationRule(u'dataDNSOpenssl', u'seven', [u'openssl']),
+            TransformationRule(u'dataDNSCertutil', u'eight', [u'certutil']),
+        ]),
+)
 
 DEFAULT_PROFILE = u'caIPAserviceCert'
 
@@ -125,7 +170,7 @@ def ca_status(ca_host=None):
 
 
 def https_request(host, port, url, secdir, password, nickname,
-        method='POST', headers=None, body=None, **kw):
+                  method='POST', headers=None, body=None, **kw):
     """
     :param method: HTTP request method (defalut: 'POST')
     :param url: The path (not complete URL!) to post to.
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 070498fe8a394802ea55f848a268e2b6563ec472..db6099ff922c645d384d675cb257283b1188e680 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1813,6 +1813,14 @@ def __get_profile_config(profile_id):
     return ipautil.template_file(
         '/usr/share/ipa/profiles/{}.cfg'.format(profile_id), sub_dict)
 
+
+def __create_entry_if_new(conn, entry):
+    if not conn.entry_exists(entry.dn):
+        conn.add_entry(entry)
+        return True
+    return False
+
+
 def import_included_profiles():
     server_id = installutils.realm_to_serverid(api.env.realm)
     dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
@@ -1834,28 +1842,66 @@ def import_included_profiles():
     api.Backend.ra_certprofile._read_password()
     api.Backend.ra_certprofile.override_port = 8443
 
-    for (profile_id, desc, store_issued) in dogtag.INCLUDED_PROFILES:
-        dn = DN(('cn', profile_id),
-            api.env.container_certprofile, api.env.basedn)
-        try:
-            conn.get_entry(dn)
-            continue  # the profile is present
-        except errors.NotFound:
-            # profile not found; add it
-            entry = conn.make_entry(
-                dn,
-                objectclass=['ipacertprofile'],
-                cn=[profile_id],
-                description=[desc],
-                ipacertprofilestoreissued=['TRUE' if store_issued else 'FALSE'],
-            )
-            conn.add_entry(entry)
+    for (ruleset_id, description,
+            transformations) in dogtag.INCLUDED_MAPPING_RULESETS:
+        ruleset_dn = DN(('cn', ruleset_id),
+                        api.env.container_certmappingruleset, api.env.basedn)
+        entry = conn.make_entry(
+            ruleset_dn,
+            objectclass=['ipacertmappingruleset'],
+            cn=[ruleset_id],
+            description=[description],
+        )
 
+        if __create_entry_if_new(conn, entry):
+            for (rule_id, template, helpers) in transformations:
+                dn = DN(('cn', rule_id), ruleset_dn)
+                entry = conn.make_entry(
+                    dn,
+                    objectclass=['ipacerttransformationrule'],
+                    cn=[rule_id],
+                    ipacerttransformationtemplate=[template],
+                    ipacerttransformationhelper=helpers,
+                )
+                __create_entry_if_new(conn, entry)
+
+    for (profile_id, desc, store_issued,
+            field_mappings) in dogtag.INCLUDED_PROFILES:
+        profile_dn = DN(('cn', profile_id),
+                        api.env.container_certprofile, api.env.basedn)
+        entry = conn.make_entry(
+            profile_dn,
+            objectclass=['ipacertprofile'],
+            cn=[profile_id],
+            description=[desc],
+            ipacertprofilestoreissued=['TRUE' if store_issued else 'FALSE'],
+        )
+
+        if __create_entry_if_new(conn, entry):
             # Create the profile, replacing any existing profile of same name
             profile_data = __get_profile_config(profile_id)
             _create_dogtag_profile(profile_id, profile_data, overwrite=True)
             root_logger.info("Imported profile '%s'", profile_id)
 
+            for index, (syntax_mapping,
+                        data_mappings) in enumerate(field_mappings):
+                fieldmapping_dn = DN(('cn', u'field%s' % index), profile_dn)
+                syntax_dn = DN(
+                    ('cn', syntax_mapping),
+                    api.env.container_certmappingruleset, api.env.basedn)
+                data_dns = [
+                    DN(('cn', rule), api.env.container_certmappingruleset,
+                        api.env.basedn)
+                    for rule in data_mappings]
+                entry = conn.make_entry(
+                    fieldmapping_dn,
+                    objectclass=['ipacertfieldmappingrule'],
+                    cn=[fieldmapping_dn['cn']],
+                    ipacertsyntaxmapping=[syntax_dn],
+                    ipacertdatamapping=data_dns,
+                )
+                __create_entry_if_new(conn, entry)
+
     api.Backend.ra_certprofile.override_port = None
     conn.disconnect()
 
-- 
2.5.5

From 158f757710bbd5b3e802dee8e38530b7d843913c Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Tue, 5 Jul 2016 14:20:00 -0400
Subject: [PATCH 4/9] Add code to support generating configs using mapping
 rules

Provides a framework that uses jinja2 to format a template for the
generated config and then substitute data from the database into it. The
rules themselves will be added in a later commit.

https://fedorahosted.org/freeipa/ticket/4899
---
 freeipa.spec.in                  |   2 +
 ipaclient/plugins/certmapping.py |  10 +-
 ipaserver/plugins/certmapping.py | 196 ++++++++++++++++++++++++++++++++++++++-
 3 files changed, 200 insertions(+), 8 deletions(-)

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 135e9c980011c6c2730c6c29a3c22098e48270d5..14f72e6975467c75951cab6675d80577452dd0ba 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -50,6 +50,7 @@ BuildRequires:  samba-devel >= %{samba_version}
 BuildRequires:  samba-python
 BuildRequires:  libtalloc-devel
 BuildRequires:  libtevent-devel
+BuildRequires:  python-jinja2
 %endif # ONLY_CLIENT
 BuildRequires:  nspr-devel
 BuildRequires:  nss-devel
@@ -214,6 +215,7 @@ Requires: dbus-python
 Requires: python-dns >= 1.11.1
 Requires: python-kdcproxy >= 0.3
 Requires: rpm-libs
+Requires: python-jinja2
 
 %description -n python2-ipaserver
 IPA is an integrated solution to provide centrally managed Identity (users,
diff --git a/ipaclient/plugins/certmapping.py b/ipaclient/plugins/certmapping.py
index 818bd940bbb979b21d7e804c23867bf8c5d16438..12d274e4c29c76a92e7cbbf644f0155aa7287b55 100644
--- a/ipaclient/plugins/certmapping.py
+++ b/ipaclient/plugins/certmapping.py
@@ -14,14 +14,16 @@ if six.PY3:
     unicode = str
 
 __doc__ = _("""
-Temporary command override to display debug data generated by
-the server-side plugin
+Command override to display the produced CSR generation data
 """)
 
 @register(override=True, no_fail=True)
 class cert_get_requestdata(CommandOverride):
     has_output_params = (
-        Str('debug_output',
-            label=_('Debug output'),
+        Str('commandline',
+            label=_('Command to run'),
+        ),
+        Str('configfile',
+            label=_('Configuration file contents'),
         ),
     )
diff --git a/ipaserver/plugins/certmapping.py b/ipaserver/plugins/certmapping.py
index d5dc33658ce49a62ad10092f6c9588ab21e270c0..ed1bcf07f16f9ba90db47b0406b184488d4a00ed 100644
--- a/ipaserver/plugins/certmapping.py
+++ b/ipaserver/plugins/certmapping.py
@@ -1,14 +1,27 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+import collections
+import jinja2
+import jinja2.ext
+import jinja2.sandbox
 
 from ipalib import api
-from ipalib import DNParam, Str, Command
+from ipalib import errors
+from ipalib import Backend, DNParam, Str, Command
 from ipalib import output
 from ipalib.parameters import Principal
 from ipalib.plugable import Registry
 from ipalib.text import _
-from .baseldap import (
-    LDAPCreate, LDAPObject, LDAPRetrieve, LDAPSearch, LDAPUpdate, LDAPDelete)
+from .baseldap import (LDAPCreate, LDAPObject, LDAPRetrieve, LDAPSearch,
+                       LDAPUpdate, LDAPDelete)
 from .certprofile import validate_profile_id
 
+import six
+
+if six.PY3:
+    unicode = str
 
 __doc__ = _("""
 Mappings from FreeIPA data to Certificate Signing Requests.
@@ -256,7 +269,182 @@ class cert_get_requestdata(Command):
         profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE)
         helper = kw.get('format')
 
-        result = {'debug_output': u'test'}
+        try:
+            if principal.is_host:
+                principal_obj = api.Command.host_show(
+                    principal.hostname, all=True)
+            elif principal.is_service:
+                principal_obj = api.Command.service_show(
+                    unicode(principal), all=True)
+            elif principal.is_user:
+                principal_obj = api.Command.user_show(
+                    principal.username, all=True)
+        except errors.NotFound:
+            raise errors.NotFound(
+                reason=_("The principal for this request doesn't exist."))
+        principal_obj = principal_obj['result']
+
+        request_data = self.Backend.certmapping.get_request_data(
+            principal_obj, profile_id, helper)
+
+        result = {}
+        result.update(request_data)
         return dict(
             result=result
         )
+
+
+class IndexableUndefined(jinja2.Undefined):
+    def __getitem__(self, key):
+        return jinja2.Undefined(
+            hint=self._undefined_hint, obj=self._undefined_obj,
+            name=self._undefined_name, exc=self._undefined_exception)
+
+
+class Formatter(object):
+    def __init__(self, backend):
+        self.backend = backend
+        self.jinja2 = jinja2.sandbox.SandboxedEnvironment(
+            loader=jinja2.FileSystemLoader('/usr/share/ipa/csrtemplates'),
+            extensions=[jinja2.ext.ExprStmtExtension],
+            keep_trailing_newline=True, undefined=IndexableUndefined)
+
+        self.passthrough_globals = {}
+        self._define_passthrough('ipa.syntaxrule')
+        self._define_passthrough('ipa.datarule')
+
+    def _define_passthrough(self, call):
+
+        def passthrough(caller):
+            return u'{%% call %s() %%}%s{%% endcall %%}' % (call, caller())
+
+        parts = call.split('.')
+        current_level = self.passthrough_globals
+        for part in parts[:-1]:
+            if part not in current_level:
+                current_level[part] = {}
+            current_level = current_level[part]
+        current_level[parts[-1]] = passthrough
+
+    def format(self, syntax_rules, render_data):
+        """
+        Combine the values into a string for a particular CSR generator.
+
+        :param syntax_rules: list of prepared syntax rules to insert into the
+            template.
+        :param render_data: dict of data from LDAP for the final render.
+
+        :returns: unicode string presenting the configuration in a form
+            suitable for input into the CSR generator.
+        """
+        raise NotImplementedError('Formatter must be subclassed before using.')
+
+    def _format(self, base_template_name, base_template_params, render_data):
+        base_template = self.jinja2.get_template(
+            base_template_name, globals=self.passthrough_globals)
+        combined_template_source = base_template.render(**base_template_params)
+        self.backend.debug(
+            'Formatting with template: %s' % combined_template_source)
+        combined_template = self.jinja2.from_string(combined_template_source)
+        return combined_template.render(**render_data)
+
+    def _wrap_rule(self, rule, rule_type):
+        template = '{%% call ipa.%srule() %%}%s{%% endcall %%}' % (
+            rule_type, rule)
+        return template
+
+    def prepare_data_rule(self, data_rule):
+        return self._wrap_rule(data_rule, 'data')
+
+    def prepare_syntax_rule(self, syntax_rule, data_rules):
+        self.backend.debug('Syntax rule template: %s' % syntax_rule)
+        template = self.jinja2.from_string(
+            syntax_rule, globals=self.passthrough_globals)
+        prepared_template = self._wrap_rule(
+            template.render(datarules=data_rules), 'syntax')
+        return prepared_template
+
+
+class OpenSSLFormatter(Formatter):
+    SyntaxRule = collections.namedtuple(
+        'SyntaxRule', ['template', 'is_extension'])
+
+    def __init__(self, backend):
+        super(OpenSSLFormatter, self).__init__(backend)
+        self._define_passthrough('openssl.section')
+
+    def format(self, syntax_rules, render_data):
+        parameters = [rule.template for rule in syntax_rules
+                      if not rule.is_extension]
+        extensions = [rule.template for rule in syntax_rules
+                      if rule.is_extension]
+
+        rendered = self._format(
+            'openssl_base.tmpl',
+            {'parameters': parameters, 'extensions': extensions}, render_data)
+        return dict(configfile=rendered)
+
+    def prepare_syntax_rule(self, syntax_rule, data_rules):
+        """Overrides method to pull out whether rule is an extension or not."""
+        self.backend.debug('Syntax rule template: %s' % syntax_rule)
+        template = self.jinja2.from_string(
+            syntax_rule, globals=self.passthrough_globals)
+        is_extension = getattr(template.module, 'extension', False)
+        prepared_template = self._wrap_rule(
+            template.render(datarules=data_rules), 'syntax')
+        return self.SyntaxRule(prepared_template, is_extension)
+
+
+class CertutilFormatter(Formatter):
+    def format(self, syntax_rules, render_data):
+        rendered = self._format(
+            'certutil_base.tmpl', {'options': syntax_rules}, render_data)
+        return dict(commandline=rendered)
+
+
+@register()
+class certmapping(Backend):
+    FORMATTERS = {
+        'openssl': OpenSSLFormatter,
+        'certutil': CertutilFormatter,
+    }
+
+    def get_request_data(self, principal, profile_id, helper):
+        config = api.Command.config_show()['result']
+        render_data = {'subject': principal, 'config': config}
+
+        formatter = self.FORMATTERS[helper](self)
+
+        syntax_rules = []
+        field_mappings = api.Command.certfieldmappingrule_find(
+            profile_id)['result']
+        for mapping in field_mappings:
+            syntax_ruleset_name = mapping['ipacertsyntaxmapping'][0]
+            syntax_ruleset = api.Command.certmappingrule_show(
+                syntax_ruleset_name['cn'])['result']
+            data_ruleset_names = mapping['ipacertdatamapping']
+            data_rulesets = [
+                api.Command.certmappingrule_show(name['cn'])['result']
+                for name in data_ruleset_names]
+
+            syntax_rule = self.get_rule_for_helper(syntax_ruleset, helper)
+            data_rules = [formatter.prepare_data_rule(
+                self.get_rule_for_helper(ruleset, helper))
+                for ruleset in data_rulesets]
+            syntax_rules.append(formatter.prepare_syntax_rule(
+                syntax_rule, data_rules))
+
+        formatted_values = formatter.format(syntax_rules, render_data)
+        return formatted_values
+
+    def get_rule_for_helper(self, ruleset, helper):
+        rules = api.Command.certtransformationrule_find(
+            ruleset['cn'][0])['result']
+        for rule in rules:
+            if helper in rule['ipacerttransformationhelper']:
+                template = rule['ipacerttransformationtemplate'][0]
+                return template
+        raise errors.NotFound(
+            reason=_('No transformation in "%(ruleset)s" rule supports'
+                     ' format "%(helper)s"') %
+            {'ruleset': ruleset['cn'][0], 'helper': helper})
-- 
2.5.5

From a01eabe12bed42f356e8d459a94238d2523a6111 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 25 Jul 2016 14:33:51 -0400
Subject: [PATCH 5/9] Add jinja2 templates and macros to support generating
 configs

https://fedorahosted.org/freeipa/ticket/4899
---
 freeipa.spec.in                                |  2 ++
 install/configure.ac                           |  1 +
 install/share/Makefile.am                      |  1 +
 install/share/csrtemplates/Makefile.am         | 17 +++++++++++
 install/share/csrtemplates/certutil_base.tmpl  |  4 +++
 install/share/csrtemplates/ipa_macros.tmpl     | 42 ++++++++++++++++++++++++++
 install/share/csrtemplates/openssl_base.tmpl   | 18 +++++++++++
 install/share/csrtemplates/openssl_macros.tmpl | 29 ++++++++++++++++++
 8 files changed, 114 insertions(+)
 create mode 100644 install/share/csrtemplates/Makefile.am
 create mode 100644 install/share/csrtemplates/certutil_base.tmpl
 create mode 100644 install/share/csrtemplates/ipa_macros.tmpl
 create mode 100644 install/share/csrtemplates/openssl_base.tmpl
 create mode 100644 install/share/csrtemplates/openssl_macros.tmpl

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 14f72e6975467c75951cab6675d80577452dd0ba..58b3011c474ed6255ff9f78d5150778d879a45b6 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -1172,6 +1172,8 @@ fi
 %{_usr}/share/ipa/advise/legacy/*.template
 %dir %{_usr}/share/ipa/profiles
 %{_usr}/share/ipa/profiles/*.cfg
+%dir %{_usr}/share/ipa/csrtemplates
+%{_usr}/share/ipa/csrtemplates/*.tmpl
 %dir %{_usr}/share/ipa/ffextension
 %{_usr}/share/ipa/ffextension/bootstrap.js
 %{_usr}/share/ipa/ffextension/install.rdf
diff --git a/install/configure.ac b/install/configure.ac
index b5f77bf8c737a437fe78ec5bfdc95599fce99760..2092c3f41caada7fc42d92d68112f5807c8b216c 100644
--- a/install/configure.ac
+++ b/install/configure.ac
@@ -88,6 +88,7 @@ AC_CONFIG_FILES([
     share/advise/Makefile
     share/advise/legacy/Makefile
     share/profiles/Makefile
+    share/csrtemplates/Makefile
     ui/Makefile
     ui/css/Makefile
     ui/src/Makefile
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index cd1c164e372e1daf8ac59bbc3f9edc10ea6a2853..27838813266d5aade232f2563ed7096905c75679 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -3,6 +3,7 @@ NULL =
 SUBDIRS =  				\
 	advise				\
 	profiles			\
+	csrtemplates			\
 	$(NULL)
 
 appdir = $(IPA_DATA_DIR)
diff --git a/install/share/csrtemplates/Makefile.am b/install/share/csrtemplates/Makefile.am
new file mode 100644
index 0000000000000000000000000000000000000000..95e9462257b6915f3f170e177b8260713b49306b
--- /dev/null
+++ b/install/share/csrtemplates/Makefile.am
@@ -0,0 +1,17 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/csrtemplates
+app_DATA =				\
+	certutil_base.tmpl		\
+	openssl_base.tmpl		\
+	openssl_macros.tmpl		\
+	ipa_macros.tmpl			\
+	$(NULL)
+
+EXTRA_DIST =				\
+	$(app_DATA)			\
+	$(NULL)
+
+MAINTAINERCLEANFILES =			\
+	*~				\
+	Makefile.in
diff --git a/install/share/csrtemplates/certutil_base.tmpl b/install/share/csrtemplates/certutil_base.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..d57273c04acede24af054c16e2128a8405bd2460
--- /dev/null
+++ b/install/share/csrtemplates/certutil_base.tmpl
@@ -0,0 +1,4 @@
+{% raw -%}
+{% import "ipa_macros.tmpl" as ipa -%}
+{%- endraw %}
+certutil -R -a {{ options|join(' ') }}
diff --git a/install/share/csrtemplates/ipa_macros.tmpl b/install/share/csrtemplates/ipa_macros.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..e790d4eb54da6d61760d179793af8712796f0788
--- /dev/null
+++ b/install/share/csrtemplates/ipa_macros.tmpl
@@ -0,0 +1,42 @@
+{% set rendersyntax = {} %}
+
+{% set renderdata = {} %}
+
+{# Wrapper for syntax rules. We render the contents of the rule into a
+variable, so that if we find that none of the contained data rules rendered we
+can suppress the whole syntax rule. That is, a syntax rule is rendered either
+if no data rules are specified (unusual) or if at least one of the data rules
+rendered successfully. #}
+{% macro syntaxrule() -%}
+{% do rendersyntax.update(none=true, any=false) -%}
+{% set contents -%}
+{{ caller() -}}
+{% endset -%}
+{% if rendersyntax['none'] or rendersyntax['any'] -%}
+{{ contents -}}
+{% endif -%}
+{% endmacro %}
+
+{# Wrapper for data rules. A data rule is rendered only when all of the data
+fields it contains have data available. #}
+{% macro datarule() -%}
+{% do rendersyntax.update(none=false) -%}
+{% do renderdata.update(all=true) -%}
+{% set contents -%}
+{{ caller() -}}
+{% endset -%}
+{% if renderdata['all'] -%}
+{% do rendersyntax.update(any=true) -%}
+{{ contents -}}
+{% endif -%}
+{% endmacro %}
+
+{# Wrapper for fields in data rules. If any value wrapped by this macro
+produces an empty string, the entire data rule will be suppressed. #}
+{% macro datafield(value) -%}
+{% if value -%}
+{{ value -}}
+{% else -%}
+{% do renderdata.update(all=false) -%}
+{% endif -%}
+{% endmacro %}
diff --git a/install/share/csrtemplates/openssl_base.tmpl b/install/share/csrtemplates/openssl_base.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..bdc58bd4afb2f55df800c19d691dd570883038de
--- /dev/null
+++ b/install/share/csrtemplates/openssl_base.tmpl
@@ -0,0 +1,18 @@
+{% raw -%}
+{% import "openssl_macros.tmpl" as openssl -%}
+{% import "ipa_macros.tmpl" as ipa -%}
+{%- endraw %}
+[ req ]
+prompt = no
+encrypt_key = no
+
+{{ parameters|join('\n') }}
+{% raw %}{% set rendered_extensions -%}{% endraw %}
+{{ extensions|join('\n') }}
+{% raw -%}
+{%- endset -%}
+{% if rendered_extensions -%}
+req_extensions = {% call openssl.section() %}{{ rendered_extensions }}{% endcall %}
+{% endif %}
+{{ openssl.openssl_sections|join('\n\n') }}
+{%- endraw %}
diff --git a/install/share/csrtemplates/openssl_macros.tmpl b/install/share/csrtemplates/openssl_macros.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..d31b8fef5f2d85e1b3d5ecf425f00ec9c22ac301
--- /dev/null
+++ b/install/share/csrtemplates/openssl_macros.tmpl
@@ -0,0 +1,29 @@
+{# List containing rendered sections to be included at end #}
+{% set openssl_sections = [] %}
+
+{#
+List containing one entry for each section name allocated. Because of
+scoping rules, we need to use a list so that it can be a "per-render global"
+that gets updated in place. Real globals are shared by all templates with the
+same environment, and variables defined in the macro don't persist after the
+macro invocation ends.
+#}
+{% set openssl_section_num = [] %}
+
+{% macro section() -%}
+{% set name -%}
+sec{{ openssl_section_num|length -}}
+{% endset -%}
+{% do openssl_section_num.append('') -%}
+{% set contents %}{{ caller() }}{% endset -%}
+{% if contents -%}
+{% set sectiondata = formatsection(name, contents) -%}
+{% do openssl_sections.append(sectiondata) -%}
+{% endif -%}
+{{ name -}}
+{% endmacro %}
+
+{% macro formatsection(name, contents) -%}
+[ {{ name }} ]
+{{ contents -}}
+{% endmacro %}
-- 
2.5.5

From e38e04a11bcb0080b340ba13fb9dc3d426100400 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 25 Jul 2016 15:15:30 -0400
Subject: [PATCH 6/9] Add jinja2 transformation rules for caIPAserviceCert

https://fedorahosted.org/freeipa/ticket/4899
---
 ipapython/dogtag.py              | 35 ++++++++++++++++++++++++--------
 ipapython/templating.py          | 44 ++++++++++++++++++++++++++++++++++++++++
 ipaserver/plugins/certmapping.py |  3 ++-
 3 files changed, 73 insertions(+), 9 deletions(-)
 create mode 100644 ipapython/templating.py

diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index 8bd53b0af2e359563778e0c3c868a621c30dda0c..8ad194daa06cebf07e3112d6a04fd7ea3bb7b159 100644
--- a/ipapython/dogtag.py
+++ b/ipapython/dogtag.py
@@ -67,28 +67,47 @@ INCLUDED_MAPPING_RULESETS = (
     MappingRuleset(
         u'syntaxSubject', u'Syntax for adding a Subject Distinguished Name',
         [
-            TransformationRule(u'syntaxSubjectOpenssl', u'one', [u'openssl']),
             TransformationRule(
-                u'syntaxSubjectCertutil', u'two', [u'certutil']),
+                u'syntaxSubjectOpenssl',
+                u'distinguished_name = {% call openssl.section() %}{{ datarules|first }}{% endcall %}',
+                [u'openssl']),
+            TransformationRule(
+                u'syntaxSubjectCertutil', u'-s {{ datarules|first }}',
+                [u'certutil']),
         ]),
     MappingRuleset(
         u'dataHostCN', u'DN with the principal\'s hostname as the CommonName',
         [
-            TransformationRule(u'dataHostOpenssl', u'three', [u'openssl']),
-            TransformationRule(u'dataHostCertutil', u'four', [u'certutil']),
+            TransformationRule(
+                u'dataHostOpenssl',
+                u'{{ipa.datafield(config.ipacertificatesubjectbase.0)}}\nCN={{ipa.datafield(subject.krbprincipalname.0|safe_attr("hostname"))}}',
+                [u'openssl']),
+            TransformationRule(
+                u'dataHostCertutil',
+                u'CN={{ipa.datafield(subject.krbprincipalname.0|safe_attr("hostname"))|quote}},{{ipa.datafield(config.ipacertificatesubjectbase.0)|quote}}',
+                [u'certutil']),
         ]),
     MappingRuleset(
         u'syntaxSAN', u'Syntax for adding a Subject Alternate Name',
         [
-            TransformationRule(u'syntaxSANOpenssl', u'five', [u'openssl']),
-            TransformationRule(u'syntaxSANCertutil', u'six', [u'certutil']),
+            TransformationRule(
+                u'syntaxSANOpenssl',
+                u'{% set extension = true %}subjectAltName = @{% call openssl.section() %}{{ datarules|join(\'\\n\') }}{% endcall %}',
+                [u'openssl']),
+            TransformationRule(
+                u'syntaxSANCertutil', u'--extSAN {{ datarules|join(\',\') }}',
+                [u'certutil']),
         ]),
     MappingRuleset(
         u'dataDNS',
         u'Constructs a SubjectAltName entry from the principal\'s hostname',
         [
-            TransformationRule(u'dataDNSOpenssl', u'seven', [u'openssl']),
-            TransformationRule(u'dataDNSCertutil', u'eight', [u'certutil']),
+            TransformationRule(
+                u'dataDNSOpenssl', u'DNS = {{ipa.datafield(subject.krbprincipalname.0|safe_attr("hostname"))}}',
+                [u'openssl']),
+            TransformationRule(
+                u'dataDNSCertutil', u'dns:{{ipa.datafield(subject.krbprincipalname.0|safe_attr("hostname"))|quote}}',
+                [u'certutil']),
         ]),
 )
 
diff --git a/ipapython/templating.py b/ipapython/templating.py
new file mode 100644
index 0000000000000000000000000000000000000000..37a9926411c7d0272515273f0b90d36c9a528839
--- /dev/null
+++ b/ipapython/templating.py
@@ -0,0 +1,44 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+import pipes
+
+from jinja2.ext import Extension
+
+
+class IPAExtension(Extension):
+    """Jinja2 extension providing useful features for cert mapping rules."""
+
+    def __init__(self, environment):
+        super(IPAExtension, self).__init__(environment)
+
+        environment.filters.update(
+            quote=self.quote,
+            safe_attr=self.safe_attr,
+        )
+
+    def quote(self, data):
+        return pipes.quote(data)
+
+    def safe_attr(self, obj, name):
+        """Get an attribute of an object, ignoring exceptions.
+
+        Works just like the attr() filter except that it returns undefined on
+        more exceptions than just AttributeError when getting the attribute.
+        """
+        try:
+            name = str(name)
+        except UnicodeError:
+            pass
+        else:
+            try:
+                value = getattr(obj, name)
+            except (AttributeError, ValueError):
+                pass
+            else:
+                if (self.environment.sandboxed and not
+                        self.environment.is_safe_attribute(obj, name, value)):
+                    return self.environment.unsafe_undefined(obj, name)
+                return value
+        return self.environment.undefined(obj=obj, name=name)
diff --git a/ipaserver/plugins/certmapping.py b/ipaserver/plugins/certmapping.py
index ed1bcf07f16f9ba90db47b0406b184488d4a00ed..13a53a667b4acaa2881c76473cdbbd689427d90f 100644
--- a/ipaserver/plugins/certmapping.py
+++ b/ipaserver/plugins/certmapping.py
@@ -14,6 +14,7 @@ from ipalib import output
 from ipalib.parameters import Principal
 from ipalib.plugable import Registry
 from ipalib.text import _
+from ipapython.templating import IPAExtension
 from .baseldap import (LDAPCreate, LDAPObject, LDAPRetrieve, LDAPSearch,
                        LDAPUpdate, LDAPDelete)
 from .certprofile import validate_profile_id
@@ -306,7 +307,7 @@ class Formatter(object):
         self.backend = backend
         self.jinja2 = jinja2.sandbox.SandboxedEnvironment(
             loader=jinja2.FileSystemLoader('/usr/share/ipa/csrtemplates'),
-            extensions=[jinja2.ext.ExprStmtExtension],
+            extensions=[jinja2.ext.ExprStmtExtension, IPAExtension],
             keep_trailing_newline=True, undefined=IndexableUndefined)
 
         self.passthrough_globals = {}
-- 
2.5.5

From 1648945c1ec2b0ab3d3d6944008fb3fddacbaaf6 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 25 Jul 2016 18:48:13 -0400
Subject: [PATCH 7/9] Add a new cert profile for users

https://fedorahosted.org/freeipa/ticket/4899
---
 install/share/profiles/Makefile.am  |  1 +
 install/share/profiles/userCert.cfg | 97 +++++++++++++++++++++++++++++++++++++
 ipapython/dogtag.py                 | 30 ++++++++++++
 3 files changed, 128 insertions(+)
 create mode 100644 install/share/profiles/userCert.cfg

diff --git a/install/share/profiles/Makefile.am b/install/share/profiles/Makefile.am
index b5ccb6e9317a93c040b7de0e0bc1ca5cb88c33fc..2e0166b91e1c85fd734f325d1c7c77598ab4ff39 100644
--- a/install/share/profiles/Makefile.am
+++ b/install/share/profiles/Makefile.am
@@ -4,6 +4,7 @@ appdir = $(IPA_DATA_DIR)/profiles
 app_DATA =				\
 	caIPAserviceCert.cfg		\
 	IECUserRoles.cfg		\
+	userCert.cfg			\
 	$(NULL)
 
 EXTRA_DIST =				\
diff --git a/install/share/profiles/userCert.cfg b/install/share/profiles/userCert.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..3c2eeb5af1cf7422d8d4dda233cfec463e848125
--- /dev/null
+++ b/install/share/profiles/userCert.cfg
@@ -0,0 +1,97 @@
+profileId=userCert
+classId=caEnrollImpl
+desc=This certificate profile is for enrolling user certificates with S/MIME key usage permitted
+visible=false
+enable=true
+enableBy=admin
+auth.instance_id=raCertAuth
+name=Manual User Dual-Use S/MIME capabilities Certificate Enrollment
+input.list=i1,i2
+input.i1.class_id=certReqInputImpl
+input.i2.class_id=submitterInfoInputImpl
+output.list=o1
+output.o1.class_id=certOutputImpl
+policyset.list=userCertSet
+policyset.userCertSet.list=1,2,3,4,5,6,7,8,9,10
+policyset.userCertSet.1.constraint.class_id=subjectNameConstraintImpl
+policyset.userCertSet.1.constraint.name=Subject Name Constraint
+policyset.userCertSet.1.constraint.params.pattern=CN=[^,]+,.+
+policyset.userCertSet.1.constraint.params.accept=true
+policyset.userCertSet.1.default.class_id=subjectNameDefaultImpl
+policyset.userCertSet.1.default.name=Subject Name Default
+policyset.userCertSet.1.default.params.name=CN=$$request.req_subject_name.cn$$, $SUBJECT_DN_O
+policyset.userCertSet.2.constraint.class_id=validityConstraintImpl
+policyset.userCertSet.2.constraint.name=Validity Constraint
+policyset.userCertSet.2.constraint.params.range=365
+policyset.userCertSet.2.constraint.params.notBeforeCheck=false
+policyset.userCertSet.2.constraint.params.notAfterCheck=false
+policyset.userCertSet.2.default.class_id=validityDefaultImpl
+policyset.userCertSet.2.default.name=Validity Default
+policyset.userCertSet.2.default.params.range=180
+policyset.userCertSet.2.default.params.startTime=0
+policyset.userCertSet.3.constraint.class_id=keyConstraintImpl
+policyset.userCertSet.3.constraint.name=Key Constraint
+policyset.userCertSet.3.constraint.params.keyType=RSA
+policyset.userCertSet.3.constraint.params.keyParameters=1024,2048,3072,4096
+policyset.userCertSet.3.default.class_id=userKeyDefaultImpl
+policyset.userCertSet.3.default.name=Key Default
+policyset.userCertSet.4.constraint.class_id=noConstraintImpl
+policyset.userCertSet.4.constraint.name=No Constraint
+policyset.userCertSet.4.default.class_id=authorityKeyIdentifierExtDefaultImpl
+policyset.userCertSet.4.default.name=Authority Key Identifier Default
+policyset.userCertSet.5.constraint.class_id=noConstraintImpl
+policyset.userCertSet.5.constraint.name=No Constraint
+policyset.userCertSet.5.default.class_id=authInfoAccessExtDefaultImpl
+policyset.userCertSet.5.default.name=AIA Extension Default
+policyset.userCertSet.5.default.params.authInfoAccessADEnable_0=true
+policyset.userCertSet.5.default.params.authInfoAccessADLocationType_0=URIName
+policyset.userCertSet.5.default.params.authInfoAccessADLocation_0=http://$IPA_CA_RECORD.$DOMAIN/ca/ocsp
+policyset.userCertSet.5.default.params.authInfoAccessADMethod_0=1.3.6.1.5.5.7.48.1
+policyset.userCertSet.5.default.params.authInfoAccessCritical=false
+policyset.userCertSet.5.default.params.authInfoAccessNumADs=1
+policyset.userCertSet.6.constraint.class_id=keyUsageExtConstraintImpl
+policyset.userCertSet.6.constraint.name=Key Usage Extension Constraint
+policyset.userCertSet.6.constraint.params.keyUsageCritical=true
+policyset.userCertSet.6.constraint.params.keyUsageDigitalSignature=true
+policyset.userCertSet.6.constraint.params.keyUsageNonRepudiation=true
+policyset.userCertSet.6.constraint.params.keyUsageDataEncipherment=false
+policyset.userCertSet.6.constraint.params.keyUsageKeyEncipherment=true
+policyset.userCertSet.6.constraint.params.keyUsageKeyAgreement=false
+policyset.userCertSet.6.constraint.params.keyUsageKeyCertSign=false
+policyset.userCertSet.6.constraint.params.keyUsageCrlSign=false
+policyset.userCertSet.6.constraint.params.keyUsageEncipherOnly=false
+policyset.userCertSet.6.constraint.params.keyUsageDecipherOnly=false
+policyset.userCertSet.6.default.class_id=keyUsageExtDefaultImpl
+policyset.userCertSet.6.default.name=Key Usage Default
+policyset.userCertSet.6.default.params.keyUsageCritical=true
+policyset.userCertSet.6.default.params.keyUsageDigitalSignature=true
+policyset.userCertSet.6.default.params.keyUsageNonRepudiation=true
+policyset.userCertSet.6.default.params.keyUsageDataEncipherment=false
+policyset.userCertSet.6.default.params.keyUsageKeyEncipherment=true
+policyset.userCertSet.6.default.params.keyUsageKeyAgreement=false
+policyset.userCertSet.6.default.params.keyUsageKeyCertSign=false
+policyset.userCertSet.6.default.params.keyUsageCrlSign=false
+policyset.userCertSet.6.default.params.keyUsageEncipherOnly=false
+policyset.userCertSet.6.default.params.keyUsageDecipherOnly=false
+policyset.userCertSet.7.constraint.class_id=noConstraintImpl
+policyset.userCertSet.7.constraint.name=No Constraint
+policyset.userCertSet.7.default.class_id=extendedKeyUsageExtDefaultImpl
+policyset.userCertSet.7.default.name=Extended Key Usage Extension Default
+policyset.userCertSet.7.default.params.exKeyUsageCritical=false
+policyset.userCertSet.7.default.params.exKeyUsageOIDs=1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.4
+policyset.userCertSet.8.constraint.class_id=signingAlgConstraintImpl
+policyset.userCertSet.8.constraint.name=No Constraint
+policyset.userCertSet.8.constraint.params.signingAlgsAllowed=SHA1withRSA,SHA256withRSA,SHA512withRSA,MD5withRSA,MD2withRSA,SHA1withDSA,SHA1withEC,SHA256withEC,SHA384withEC,SHA512withEC
+policyset.userCertSet.8.default.class_id=signingAlgDefaultImpl
+policyset.userCertSet.8.default.name=Signing Alg
+policyset.userCertSet.8.default.params.signingAlg=-
+policyset.userCertSet.9.constraint.class_id=noConstraintImpl
+policyset.userCertSet.9.constraint.name=No Constraint
+policyset.userCertSet.9.default.class_id=subjectKeyIdentifierExtDefaultImpl
+policyset.userCertSet.9.default.name=Subject Key Identifier Extension Default
+policyset.userCertSet.9.default.params.critical=false
+policyset.userCertSet.10.constraint.class_id=noConstraintImpl
+policyset.userCertSet.10.constraint.name=No Constraint
+policyset.userCertSet.10.default.class_id=userExtensionDefaultImpl
+policyset.userCertSet.10.default.name=User Supplied Extension Default
+policyset.userCertSet.10.default.params.userExtOID=2.5.29.17
diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index 8ad194daa06cebf07e3112d6a04fd7ea3bb7b159..7d68fb7100396a6cab3e52ce75e77eb86c2e1e64 100644
--- a/ipapython/dogtag.py
+++ b/ipapython/dogtag.py
@@ -57,6 +57,12 @@ INCLUDED_PROFILES = (
             FieldMapping(u'syntaxSAN', [u'dataDNS']),
         ]),
     Profile(
+        u'userCert', u'Standard profile for users', True,
+        [
+            FieldMapping(u'syntaxSubject', [u'dataUsernameCN']),
+            FieldMapping(u'syntaxSAN', [u'dataEmail']),
+        ]),
+    Profile(
         u'IECUserRoles',
         u'User profile that includes IECUserRoles extension from request',
         True, []),
@@ -88,6 +94,19 @@ INCLUDED_MAPPING_RULESETS = (
                 [u'certutil']),
         ]),
     MappingRuleset(
+        u'dataUsernameCN',
+        u'DN with the principal\'s username as the CommonName',
+        [
+            TransformationRule(
+                u'dataUsernameOpenssl',
+                u'{{ipa.datafield(config.ipacertificatesubjectbase.0)}}\nUID={{ipa.datafield(subject.uid.0)}}',
+                [u'openssl']),
+            TransformationRule(
+                u'dataUsernameCertutil',
+                u'UID={{ipa.datafield(subject.uid.0)|quote}},{{ipa.datafield(config.ipacertificatesubjectbase.0)|quote}}',
+                [u'certutil']),
+        ]),
+    MappingRuleset(
         u'syntaxSAN', u'Syntax for adding a Subject Alternate Name',
         [
             TransformationRule(
@@ -109,6 +128,17 @@ INCLUDED_MAPPING_RULESETS = (
                 u'dataDNSCertutil', u'dns:{{ipa.datafield(subject.krbprincipalname.0|safe_attr("hostname"))|quote}}',
                 [u'certutil']),
         ]),
+    MappingRuleset(
+        u'dataEmail',
+        u'Constructs a SubjectAltName entry from the principal\'s email',
+        [
+            TransformationRule(
+                u'dataEmailOpenssl', u'email = {{ipa.datafield(subject.mail.0)}}',
+                [u'openssl']),
+            TransformationRule(
+                u'dataEmailCertutil', u'email:{{ipa.datafield(subject.mail.0)|quote}}',
+                [u'certutil']),
+        ]),
 )
 
 DEFAULT_PROFILE = u'caIPAserviceCert'
-- 
2.5.5

From f9ab0a4355a7fb50e62bad6992c5c39256c277eb Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 25 Jul 2016 18:27:02 -0400
Subject: [PATCH 8/9] Add ability to import/export mappings with profile

The certprofile_show command now takes a --mappings-out flag that writes
mappings to a JSON-formatted file, and the same file format can be read
in with the --mappings-file parameter of certprofile_import and
certprofile_mod.

The exception handling here is a bit complicated, but the new
exc_callback functions should take care of making sure that state is
restored in the event of a failure. (And with the addition of an
exc_callback, overriding execute() is no longer necessary.)

https://fedorahosted.org/freeipa/ticket/4899
---
 API.txt                          |   9 ++--
 ipaclient/plugins/certprofile.py |  32 +++++++++---
 ipaserver/plugins/certmapping.py | 102 +++++++++++++++++++++++++++++++++++++++
 ipaserver/plugins/certprofile.py |  77 ++++++++++++++++++++++++-----
 4 files changed, 198 insertions(+), 22 deletions(-)

diff --git a/API.txt b/API.txt
index f317e5847cdbe881ac99caaa69882786fdd7cbdb..693f0de5b00c55bcba4305d20c3379ecafb80a02 100644
--- a/API.txt
+++ b/API.txt
@@ -950,19 +950,20 @@ output: ListOfEntries('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: Output('truncated', type=[<type 'bool'>])
 command: certprofile_import/1
-args: 1,6,3
+args: 1,7,3
 arg: Str('cn', cli_name='id')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('description', cli_name='desc')
 option: Str('file', cli_name='file')
 option: Bool('ipacertprofilestoreissued', cli_name='store', default=True)
+option: Str('mappings_file?', cli_name='mappings_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: certprofile_mod/1
-args: 1,10,3
+args: 1,11,3
 arg: Str('cn', cli_name='id')
 option: Str('addattr*', cli_name='addattr')
 option: Flag('all', autofill=True, cli_name='all', default=False)
@@ -970,6 +971,7 @@ option: Str('delattr*', cli_name='delattr')
 option: Str('description?', autofill=False, cli_name='desc')
 option: Str('file?', cli_name='file')
 option: Bool('ipacertprofilestoreissued?', autofill=False, cli_name='store', default=True)
+option: Str('mappings_file?', cli_name='mappings_file')
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Flag('rights', autofill=True, default=False)
 option: Str('setattr*', cli_name='setattr')
@@ -978,9 +980,10 @@ output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: certprofile_show/1
-args: 1,5,3
+args: 1,6,3
 arg: Str('cn', cli_name='id')
 option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('mappings_out?')
 option: Str('out?')
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Flag('rights', autofill=True, default=False)
diff --git a/ipaclient/plugins/certprofile.py b/ipaclient/plugins/certprofile.py
index cde039a9eda949a2f910dc4fa6cf2ef76b09e832..7f0f6dc19c4e9a6a9d3aa615529f6b8cf3c73253 100644
--- a/ipaclient/plugins/certprofile.py
+++ b/ipaclient/plugins/certprofile.py
@@ -8,6 +8,11 @@ from ipalib.parameters import File
 from ipalib.plugable import Registry
 from ipalib.text import _
 
+import six
+
+if six.PY3:
+    unicode = str
+
 register = Registry()
 
 
@@ -16,24 +21,39 @@ class certprofile_show(MethodOverride):
     def forward(self, *keys, **options):
         if 'out' in options:
             util.check_writable_file(options['out'])
+        if 'mappings_out' in options:
+            util.check_writable_file(options['mappings_out'])
 
         result = super(certprofile_show, self).forward(*keys, **options)
+
         if 'out' in options and 'config' in result['result']:
             with open(options['out'], 'wb') as f:
                 f.write(result['result'].pop('config'))
-            result['summary'] = (
-                _("Profile configuration stored in file '%(file)s'")
-                % dict(file=options['out'])
-            )
+        if 'mappings_out' in options and 'mappings' in result['result']:
+            with open(options['mappings_out'], 'wb') as f:
+                f.write(result['result'].pop('mappings'))
 
         return result
 
+    def output_for_cli(self, textui, output, *args, **options):
+        rv = super(certprofile_show, self).output_for_cli(
+            textui, output, *args, **options)
+
+        if 'out' in options:
+            textui.print_attribute(
+                unicode(_('Profile configuration stored to')), options['out'])
+        if 'mappings_out' in options:
+            textui.print_attribute(
+                unicode(_('Mapping rules stored to')), options['mappings_out'])
+
+        return rv
+
 
 @register(override=True, no_fail=True)
 class certprofile_import(MethodOverride):
     def get_options(self):
         for option in super(certprofile_import, self).get_options():
-            if option.name == 'file':
+            if option.name in ['file', 'mappings_file']:
                 option = option.clone_retype(option.name, File)
             yield option
 
@@ -42,6 +62,6 @@ class certprofile_import(MethodOverride):
 class certprofile_mod(MethodOverride):
     def get_options(self):
         for option in super(certprofile_mod, self).get_options():
-            if option.name == 'file':
+            if option.name in ['file', 'mappings_file']:
                 option = option.clone_retype(option.name, File)
             yield option
diff --git a/ipaserver/plugins/certmapping.py b/ipaserver/plugins/certmapping.py
index 13a53a667b4acaa2881c76473cdbbd689427d90f..c50970dd21e52d91410091ccbc40a36d75f7738e 100644
--- a/ipaserver/plugins/certmapping.py
+++ b/ipaserver/plugins/certmapping.py
@@ -6,6 +6,7 @@ import collections
 import jinja2
 import jinja2.ext
 import jinja2.sandbox
+import json
 
 from ipalib import api
 from ipalib import errors
@@ -449,3 +450,104 @@ class certmapping(Backend):
             reason=_('No transformation in "%(ruleset)s" rule supports'
                      ' format "%(helper)s"') %
             {'ruleset': ruleset['cn'][0], 'helper': helper})
+
+    def get_profile_mappings(self, profile_id):
+        """Return the list DNs for the certfieldmappingrules of a profile.
+
+        If the profile does not exist, returns an empty list.
+        """
+        mappings = []
+        try:
+            rules = api.Command.certfieldmappingrule_find(
+                profile_id)['result']
+            mappings = [rule['dn'] for rule in rules]
+        except (errors.NotFound, KeyError):
+            pass
+
+        return mappings
+
+    def delete_profile_mappings(self, profile_id, mapping_dns):
+        """Try to delete all the specified certfieldmappingrules.
+
+        If one of the specified rules does not exist, continue on to the
+        others.
+        """
+        for mapping in mapping_dns:
+            try:
+                api.Command.certfieldmappingrule_del(
+                    profile_id, mapping['cn'])
+            except errors.NotFound:
+                pass
+
+    def export_profile_mappings(self, profile_id):
+        rules = []
+        mappings = api.Command.certfieldmappingrule_find(
+            profile_id)['result']
+        for mapping in mappings:
+            syntax = mapping['ipacertsyntaxmapping'][0]['cn']
+            data = [rule['cn'] for rule in mapping['ipacertdatamapping']]
+            rules.append({'syntax': syntax, 'data': data})
+        return rules
+
+    def export_profile_mappings_json(self, profile_id):
+        rules = self.export_profile_mappings(profile_id)
+        return json.dumps(rules, indent=4) + '\n'
+
+    def _get_dn(self, cn):
+        mapping = api.Command.certmappingrule_show(cn)
+        return mapping['result']['dn']
+
+    def import_profile_mappings_json(self, profile_id, mappings_str):
+        try:
+            mappings = json.loads(mappings_str)
+        except ValueError:
+            raise errors.ValidationError(
+                name=_('mappings_file'), error=_('Not a valid JSON document'))
+
+        return self.import_profile_mappings(profile_id, mappings)
+
+    def import_profile_mappings(self, profile_id, mappings):
+        # Validate user input
+        if not isinstance(mappings, list):
+            raise errors.ValidationError(
+                name=_('mappings_file'), error=_('Must be a JSON array'))
+        for mapping in mappings:
+            if 'syntax' not in mapping:
+                raise errors.ValidationError(
+                    name=_('mappings_file'), error=_('Missing "syntax" key'))
+            if 'data' not in mapping:
+                raise errors.ValidationError(
+                    name=_('mappings_file'), error=_('Missing "data" key'))
+            if not isinstance(mapping['data'], list):
+                raise errors.ValidationError(
+                    name=_('mappings_file'),
+                    error=_('"data" key must be an array'))
+
+        old_maxindex = 0
+        # Find the highest-numbered field rule named "field<integer>"
+        for old_mapping in self.get_profile_mappings(profile_id):
+            _empty, _field, index_str = old_mapping['cn'].rpartition('field')
+            try:
+                index = int(index_str)
+            except ValueError:
+                continue
+            if index > old_maxindex:
+                old_maxindex = index
+
+        mapping_names = [u'field%s' % (old_maxindex + index + 1)
+                         for index in range(len(mappings))]
+
+        field_mappings = []
+        try:
+            for name, mapping in zip(mapping_names, mappings):
+                syntax = self._get_dn(mapping['syntax'])
+                data = [self._get_dn(rule) for rule in mapping['data']]
+                field_mapping = api.Command.certfieldmappingrule_add(
+                    profile_id, name, ipacertsyntaxmapping=syntax,
+                    ipacertdatamapping=data)['result']
+                field_mappings.append(field_mapping['dn'])
+        except:
+            self.delete_profile_mappings(profile_id, field_mappings)
+            raise
+
+        return field_mappings
diff --git a/ipaserver/plugins/certprofile.py b/ipaserver/plugins/certprofile.py
index f4466077484591c8e941027fa8e4897602384f7c..88a8b92ab7c09e81089e58eb85b44965f839ad79 100644
--- a/ipaserver/plugins/certprofile.py
+++ b/ipaserver/plugins/certprofile.py
@@ -182,7 +182,6 @@ class certprofile(LDAPObject):
     }
 
 
-
 @register()
 class certprofile_find(LDAPSearch):
     __doc__ = _("Search for Certificate Profiles.")
@@ -203,6 +202,9 @@ class certprofile_show(LDAPRetrieve):
         Str('out?',
             doc=_('Write profile configuration to file'),
         ),
+        Str('mappings_out?',
+            doc=_('Write CSR generation mappings to file'),
+        ),
     )
 
     def execute(self, *keys, **options):
@@ -213,6 +215,11 @@ class certprofile_show(LDAPRetrieve):
             with self.api.Backend.ra_certprofile as profile_api:
                 result['result']['config'] = profile_api.read_profile(keys[0])
 
+        if 'mappings_out' in options:
+            mapping_api = self.api.Backend.certmapping
+            mappings = mapping_api.export_profile_mappings_json(keys[0])
+            result['result']['mappings'] = mappings
+
         return result
 
 
@@ -228,11 +235,19 @@ class certprofile_import(LDAPCreate):
             flags=('virtual_attribute',),
             noextrawhitespace=False,
         ),
+        Str(
+            'mappings_file?',
+            label=_('Filename of a JSON file specifying CSR mapping rules.'),
+            cli_name='mappings_file',
+            flags=('virtual_attribute',),
+            noextrawhitespace=False,
+        ),
     )
 
     PROFILE_ID_PATTERN = re.compile('^profileId=([a-zA-Z]\w*)', re.MULTILINE)
 
-    def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options):
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
+                     **options):
         ca_enabled_check()
         context.profile = options['file']
 
@@ -247,7 +262,6 @@ class certprofile_import(LDAPCreate):
             )
         return dn
 
-
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         """Import the profile into Dogtag and enable it.
 
@@ -262,6 +276,17 @@ class certprofile_import(LDAPCreate):
             ldap.delete_entry(dn)
             raise
 
+        mappings = options.get('mappings_file')
+        if mappings is not None:
+            backend = self.api.Backend.certmapping
+            try:
+                backend.import_profile_mappings_json(keys[0], mappings)
+            except:
+                # At this point profile is sufficiently created that the del
+                # command should work
+                self.api.Command.certprofile_del(keys[0])
+                raise
+
         return dn
 
 
@@ -301,6 +326,13 @@ class certprofile_mod(LDAPUpdate):
             flags=('virtual_attribute',),
             noextrawhitespace=False,
         ),
+        Str(
+            'mappings_file?',
+            label=_('Filename of a JSON file specifying CSR mapping rules.'),
+            cli_name='mappings_file',
+            flags=('virtual_attribute',),
+            noextrawhitespace=False,
+        ),
     )
 
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
@@ -319,15 +351,34 @@ class certprofile_mod(LDAPUpdate):
 
         return dn
 
-    def execute(self, *keys, **options):
-        try:
-            return super(certprofile_mod, self).execute(*keys, **options)
-        except errors.EmptyModlist:
-            if 'file' in options:
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        mappings = options.get('mappings_file')
+
+        if mappings is not None:
+            backend = self.api.Backend.certmapping
+            context.old_mapping_names = backend.get_profile_mappings(keys[0])
+            context.new_mapping_names = backend.import_profile_mappings_json(
+                keys[0], mappings)
+            backend.delete_profile_mappings(
+                keys[0], context.old_mapping_names)
+
+        return dn
+
+    def exc_callback(self, keys, options, exc, call_func, *call_args,
+                     **call_kwargs):
+        mappings = options.get('mappings_file')
+        # Make sure this is really an error
+        if isinstance(exc, errors.EmptyModlist):
+            if 'file' in options or mappings is not None:
                 # The profile data in Dogtag was updated.
                 # Do not fail; return result of certprofile-show instead
-                return self.api.Command.certprofile_show(keys[0],
-                    version=API_VERSION)
-            else:
-                # This case is actually an error; re-raise
-                raise
+                return self.api.Command.certprofile_show(
+                    keys[0], version=API_VERSION)
+
+        # Clean up
+        if hasattr(context, 'new_mapping_names'):
+            self.api.Backend.certmapping.delete_profile_mappings(
+                keys[0], context.new_mapping_names)
+
+        # Re-raise exception
+        raise exc
-- 
2.5.5

From dde57babfd976a96024ce922607133b789abc116 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Tue, 12 Jul 2016 13:18:05 -0400
Subject: [PATCH 9/9] Add tests for mapping rules import/export

Also, for all import calls that are supposed to fail, we now assert that
the objects do not exist after the failed import.

https://fedorahosted.org/freeipa/ticket/4899
---
 .../data/caIPAserviceCert.mappings.json            |  14 ++
 ipatests/test_xmlrpc/data/nameless.cfg.tmpl        | 108 ++++++++++++++
 ipatests/test_xmlrpc/test_certprofile_plugin.py    | 158 +++++++++++++++++++++
 ipatests/test_xmlrpc/tracker/certprofile_plugin.py |  45 +++++-
 4 files changed, 319 insertions(+), 6 deletions(-)
 create mode 100644 ipatests/test_xmlrpc/data/caIPAserviceCert.mappings.json
 create mode 100644 ipatests/test_xmlrpc/data/nameless.cfg.tmpl

diff --git a/ipatests/test_xmlrpc/data/caIPAserviceCert.mappings.json b/ipatests/test_xmlrpc/data/caIPAserviceCert.mappings.json
new file mode 100644
index 0000000000000000000000000000000000000000..f3839b14a906442edc59a65905f781bfb9c9fe7a
--- /dev/null
+++ b/ipatests/test_xmlrpc/data/caIPAserviceCert.mappings.json
@@ -0,0 +1,14 @@
+[
+    {
+        "data": [
+            "dataHostCN"
+        ],
+        "syntax": "syntaxSubject"
+    },
+    {
+        "data": [
+            "dataDNS"
+        ],
+        "syntax": "syntaxSAN"
+    }
+]
diff --git a/ipatests/test_xmlrpc/data/nameless.cfg.tmpl b/ipatests/test_xmlrpc/data/nameless.cfg.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..f9e8ce441df5c89dd779531f1a359156c78eeaa2
--- /dev/null
+++ b/ipatests/test_xmlrpc/data/nameless.cfg.tmpl
@@ -0,0 +1,108 @@
+auth.instance_id=raCertAuth
+classId=caEnrollImpl
+visible=false
+desc=This certificate profile is for enrolling server certificates with IPA-RA agent authentication.
+enable=true
+enableBy=ipara
+input.i1.class_id=certReqInputImpl
+input.i2.class_id=submitterInfoInputImpl
+input.list=i1,i2
+name=IPA-RA Agent-Authenticated Server Certificate Enrollment
+output.list=o1
+output.o1.class_id=certOutputImpl
+policyset.list=serverCertSet
+policyset.serverCertSet.1.constraint.class_id=subjectNameConstraintImpl
+policyset.serverCertSet.1.constraint.name=Subject Name Constraint
+policyset.serverCertSet.1.constraint.params.accept=true
+policyset.serverCertSet.1.constraint.params.pattern=CN=[^,]+,.+
+policyset.serverCertSet.1.default.class_id=subjectNameDefaultImpl
+policyset.serverCertSet.1.default.name=Subject Name Default
+policyset.serverCertSet.1.default.params.name=CN=$request.req_subject_name.cn$, {ipacertbase}
+policyset.serverCertSet.10.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.10.constraint.name=No Constraint
+policyset.serverCertSet.10.default.class_id=subjectKeyIdentifierExtDefaultImpl
+policyset.serverCertSet.10.default.name=Subject Key Identifier Extension Default
+policyset.serverCertSet.10.default.params.critical=false
+policyset.serverCertSet.11.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.11.constraint.name=No Constraint
+policyset.serverCertSet.11.default.class_id=userExtensionDefaultImpl
+policyset.serverCertSet.11.default.name=User Supplied Extension Default
+policyset.serverCertSet.11.default.params.userExtOID=2.5.29.17
+policyset.serverCertSet.2.constraint.class_id=validityConstraintImpl
+policyset.serverCertSet.2.constraint.name=Validity Constraint
+policyset.serverCertSet.2.constraint.params.notAfterCheck=false
+policyset.serverCertSet.2.constraint.params.notBeforeCheck=false
+policyset.serverCertSet.2.constraint.params.range=740
+policyset.serverCertSet.2.default.class_id=validityDefaultImpl
+policyset.serverCertSet.2.default.name=Validity Default
+policyset.serverCertSet.2.default.params.range=731
+policyset.serverCertSet.2.default.params.startTime=0
+policyset.serverCertSet.3.constraint.class_id=keyConstraintImpl
+policyset.serverCertSet.3.constraint.name=Key Constraint
+policyset.serverCertSet.3.constraint.params.keyParameters=1024,2048,3072,4096
+policyset.serverCertSet.3.constraint.params.keyType=RSA
+policyset.serverCertSet.3.default.class_id=userKeyDefaultImpl
+policyset.serverCertSet.3.default.name=Key Default
+policyset.serverCertSet.4.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.4.constraint.name=No Constraint
+policyset.serverCertSet.4.default.class_id=authorityKeyIdentifierExtDefaultImpl
+policyset.serverCertSet.4.default.name=Authority Key Identifier Default
+policyset.serverCertSet.5.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.5.constraint.name=No Constraint
+policyset.serverCertSet.5.default.class_id=authInfoAccessExtDefaultImpl
+policyset.serverCertSet.5.default.name=AIA Extension Default
+policyset.serverCertSet.5.default.params.authInfoAccessADEnable_0=true
+policyset.serverCertSet.5.default.params.authInfoAccessADLocationType_0=URIName
+policyset.serverCertSet.5.default.params.authInfoAccessADLocation_0=http://ipa-ca.{ipadomain}/ca/ocsp
+policyset.serverCertSet.5.default.params.authInfoAccessADMethod_0=1.3.6.1.5.5.7.48.1
+policyset.serverCertSet.5.default.params.authInfoAccessCritical=false
+policyset.serverCertSet.5.default.params.authInfoAccessNumADs=1
+policyset.serverCertSet.6.constraint.class_id=keyUsageExtConstraintImpl
+policyset.serverCertSet.6.constraint.name=Key Usage Extension Constraint
+policyset.serverCertSet.6.constraint.params.keyUsageCritical=true
+policyset.serverCertSet.6.constraint.params.keyUsageCrlSign=false
+policyset.serverCertSet.6.constraint.params.keyUsageDataEncipherment=true
+policyset.serverCertSet.6.constraint.params.keyUsageDecipherOnly=false
+policyset.serverCertSet.6.constraint.params.keyUsageDigitalSignature=true
+policyset.serverCertSet.6.constraint.params.keyUsageEncipherOnly=false
+policyset.serverCertSet.6.constraint.params.keyUsageKeyAgreement=false
+policyset.serverCertSet.6.constraint.params.keyUsageKeyCertSign=false
+policyset.serverCertSet.6.constraint.params.keyUsageKeyEncipherment=true
+policyset.serverCertSet.6.constraint.params.keyUsageNonRepudiation=true
+policyset.serverCertSet.6.default.class_id=keyUsageExtDefaultImpl
+policyset.serverCertSet.6.default.name=Key Usage Default
+policyset.serverCertSet.6.default.params.keyUsageCritical=true
+policyset.serverCertSet.6.default.params.keyUsageCrlSign=false
+policyset.serverCertSet.6.default.params.keyUsageDataEncipherment=true
+policyset.serverCertSet.6.default.params.keyUsageDecipherOnly=false
+policyset.serverCertSet.6.default.params.keyUsageDigitalSignature=true
+policyset.serverCertSet.6.default.params.keyUsageEncipherOnly=false
+policyset.serverCertSet.6.default.params.keyUsageKeyAgreement=false
+policyset.serverCertSet.6.default.params.keyUsageKeyCertSign=false
+policyset.serverCertSet.6.default.params.keyUsageKeyEncipherment=true
+policyset.serverCertSet.6.default.params.keyUsageNonRepudiation=true
+policyset.serverCertSet.7.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.7.constraint.name=No Constraint
+policyset.serverCertSet.7.default.class_id=extendedKeyUsageExtDefaultImpl
+policyset.serverCertSet.7.default.name=Extended Key Usage Extension Default
+policyset.serverCertSet.7.default.params.exKeyUsageCritical=false
+policyset.serverCertSet.7.default.params.exKeyUsageOIDs=1.3.6.1.5.5.7.3.1,1.3.6.1.5.5.7.3.2
+policyset.serverCertSet.8.constraint.class_id=signingAlgConstraintImpl
+policyset.serverCertSet.8.constraint.name=No Constraint
+policyset.serverCertSet.8.constraint.params.signingAlgsAllowed=SHA1withRSA,SHA256withRSA,SHA512withRSA,MD5withRSA,MD2withRSA,SHA1withDSA,SHA1withEC,SHA256withEC,SHA384withEC,SHA512withEC
+policyset.serverCertSet.8.default.class_id=signingAlgDefaultImpl
+policyset.serverCertSet.8.default.name=Signing Alg
+policyset.serverCertSet.8.default.params.signingAlg=-
+policyset.serverCertSet.9.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.9.constraint.name=No Constraint
+policyset.serverCertSet.9.default.class_id=crlDistributionPointsExtDefaultImpl
+policyset.serverCertSet.9.default.name=CRL Distribution Points Extension Default
+policyset.serverCertSet.9.default.params.crlDistPointsCritical=false
+policyset.serverCertSet.9.default.params.crlDistPointsEnable_0=true
+policyset.serverCertSet.9.default.params.crlDistPointsIssuerName_0=CN=Certificate Authority,o=ipaca
+policyset.serverCertSet.9.default.params.crlDistPointsIssuerType_0=DirectoryName
+policyset.serverCertSet.9.default.params.crlDistPointsNum=1
+policyset.serverCertSet.9.default.params.crlDistPointsPointName_0=http://ipa-ca.{ipadomain}/ipa/crl/MasterCRL.bin
+policyset.serverCertSet.9.default.params.crlDistPointsPointType_0=URIName
+policyset.serverCertSet.9.default.params.crlDistPointsReasons_0=
+policyset.serverCertSet.list=1,2,3,4,5,6,7,8,9,10,11
diff --git a/ipatests/test_xmlrpc/test_certprofile_plugin.py b/ipatests/test_xmlrpc/test_certprofile_plugin.py
index e8459772d7a0b53b80b9cfa08a08dd57e4e12a47..4f5ce99b4e920b65070be26725dac4ea4cc5e4ac 100644
--- a/ipatests/test_xmlrpc/test_certprofile_plugin.py
+++ b/ipatests/test_xmlrpc/test_certprofile_plugin.py
@@ -38,6 +38,9 @@ CA_IPA_SERVICE_MALFORMED_TEMPLATE = os.path.join(
 CA_IPA_SERVICE_XML_TEMPLATE = os.path.join(
     BASE_DIR, 'data/caIPAserviceCert.xml.tmpl')
 
+NAMELESS_TEMPLATE = os.path.join(
+    BASE_DIR, 'data/nameless.cfg.tmpl')
+
 RENAME_ERR_TEMPL = (
     u'certprofile {} cannot be deleted/modified: '
     'Certificate profiles cannot be renamed')
@@ -70,6 +73,89 @@ def user_profile(request):
 
 
 @pytest.fixture(scope='class')
+def mappings_profile(request):
+    name = 'caIPAserviceCert_mappings'
+    profile_path = prepare_config(
+        NAMELESS_TEMPLATE,
+        dict(
+            ipadomain=api.env.domain,
+            ipacertbase=IPA_CERT_SUBJ_BASE))
+
+    mappings = [
+        {
+            "syntax": "syntaxSubject",
+            "data": [
+                "dataHostCN",
+            ],
+        },
+        {
+            "syntax": "syntaxSAN",
+            "data": [
+                "dataDNS",
+            ],
+        },
+    ]
+
+    tracker = CertprofileTracker(
+        name, store=True, desc=u'Profile with mappings',
+        profile=profile_path, mappings=mappings
+    )
+
+    return tracker.make_fixture(request)
+
+
+@pytest.fixture(scope='class')
+def malformed_mappings_profile(request):
+    name = 'caIPAserviceCert_bad_mappings'
+    profile_path = prepare_config(
+        NAMELESS_TEMPLATE,
+        dict(
+            ipadomain=api.env.domain,
+            ipacertbase=IPA_CERT_SUBJ_BASE))
+
+    mappings = [
+        {
+            "syntax": "syntaxSubject",
+        },
+    ]
+
+    tracker = CertprofileTracker(
+        name, store=True, desc=u'Profile with malformed mappings',
+        profile=profile_path, mappings=mappings
+    )
+
+    # Do not return with finalizer. There should be nothing to delete
+    return tracker
+
+
+@pytest.fixture(scope='class')
+def nonexistent_mappings_profile(request):
+    name = 'caIPAserviceCert_nonexistent_mappings'
+    profile_path = prepare_config(
+        NAMELESS_TEMPLATE,
+        dict(
+            ipadomain=api.env.domain,
+            ipacertbase=IPA_CERT_SUBJ_BASE))
+
+    mappings = [
+        {
+            "syntax": "nonexistentSyntax",
+            "data": [
+                "dataHostCN",
+            ],
+        },
+    ]
+
+    tracker = CertprofileTracker(
+        name, store=True, desc=u'Profile referencing nonexistent mappings',
+        profile=profile_path, mappings=mappings
+    )
+
+    # Do not return with finalizer. There should be nothing to delete
+    return tracker
+
+
+@pytest.fixture(scope='class')
 def malformed(request):
     name = u'caIPAserviceCert_mal'
     profile_path = prepare_config(
@@ -211,10 +297,79 @@ class TestProfileCRUD(XMLRPC_test):
 
 
 @pytest.mark.tier1
+class TestMappingProfileCRUD(XMLRPC_test):
+    def test_import(self, mappings_profile):
+        mappings_profile.ensure_exists()
+
+    def test_delete(self, mappings_profile):
+        mappings_profile.ensure_exists()
+        mappings_profile.delete()
+
+    def test_retrieve_simple(self, mappings_profile):
+        mappings_profile.retrieve()
+
+    def test_retrieve_all(self, mappings_profile):
+        mappings_profile.retrieve(all=True)
+
+    def test_import_mappings(self, tmpdir, mappings_profile):
+        mappings_profile.ensure_exists()
+        mappings_profile.check_mappings(tmpdir)
+
+    def test_update_remove_mappings(self, tmpdir, mappings_profile):
+        new_mappings = []
+        mappings_profile.update(dict(), mappings=new_mappings)
+        mappings_profile.check_mappings(tmpdir)
+
+    def test_update_mappings(self, tmpdir, mappings_profile):
+        new_mappings = [
+            {
+                "syntax": "syntaxSubject",
+                "data": [
+                    "dataUsernameCN"
+                ]
+            },
+            {
+                "syntax": "syntaxSAN",
+                "data": [
+                    "dataEmail"
+                ],
+            }
+        ]
+        mappings_profile.update(dict(), mappings=new_mappings)
+        mappings_profile.check_mappings(tmpdir)
+
+    def test_import_malformed(self, malformed_mappings_profile):
+        with pytest.raises(errors.ValidationError):
+            malformed_mappings_profile.ensure_exists()
+        command = malformed_mappings_profile.make_retrieve_command()
+        with pytest.raises(errors.NotFound):
+            command()
+
+    def test_import_nonexistent(self, nonexistent_mappings_profile):
+        with pytest.raises(errors.NotFound):
+            nonexistent_mappings_profile.ensure_exists()
+        command = nonexistent_mappings_profile.make_retrieve_command()
+        with pytest.raises(errors.NotFound):
+            command()
+
+    def test_create_duplicate(self, tmpdir, mappings_profile):
+        msg = u'Certificate Profile with name "{}" already exists'
+        mappings_profile.ensure_exists()
+        command = mappings_profile.make_create_command(force=True)
+        with raises_exact(errors.DuplicateEntry(
+                message=msg.format(mappings_profile.name))):
+            command()
+        mappings_profile.check_mappings(tmpdir)
+
+
+@pytest.mark.tier1
 class TestMalformedProfile(XMLRPC_test):
     def test_malformed_import(self, malformed):
         with pytest.raises(errors.ExecutionError):
             malformed.create()
+        command = malformed.make_retrieve_command()
+        with pytest.raises(errors.NotFound):
+            command()
 
 
 @pytest.mark.tier1
@@ -222,3 +377,6 @@ class TestImportFromXML(XMLRPC_test):
     def test_import_xml(self, xmlprofile):
         with pytest.raises(errors.ExecutionError):
             xmlprofile.ensure_exists()
+        command = xmlprofile.make_retrieve_command()
+        with pytest.raises(errors.NotFound):
+            command()
diff --git a/ipatests/test_xmlrpc/tracker/certprofile_plugin.py b/ipatests/test_xmlrpc/tracker/certprofile_plugin.py
index 21c96c5eb36bfeacdcae9f1df61d2f6f4aafa7e9..10328114e64c6b5d249da3cbfa6b292e2e7f0699 100644
--- a/ipatests/test_xmlrpc/tracker/certprofile_plugin.py
+++ b/ipatests/test_xmlrpc/tracker/certprofile_plugin.py
@@ -3,8 +3,10 @@
 # Copyright (C) 2015  FreeIPA Contributors see COPYING for license
 #
 
+import json
 import os
 
+import nose
 import six
 
 from ipapython.dn import DN
@@ -28,15 +30,19 @@ class CertprofileTracker(Tracker):
     update_keys = retrieve_keys - {'dn'}
     managedby_keys = retrieve_keys
     allowedto_keys = retrieve_keys
+    # Keys that are passed to Tracker.update() but never returned in the
+    # response
+    ignore_extra_keys = {'mappings_file'}
 
     def __init__(self, name, store=False, desc='dummy description',
-                 profile=None, default_version=None):
+                 profile=None, mappings=None, default_version=None):
         super(CertprofileTracker, self).__init__(
             default_version=default_version
         )
 
         self.store = store
         self.description = desc
+        self.mappings = mappings
         self._profile_path = profile
 
         self.dn = DN(('cn', name), 'cn=certprofiles', 'cn=ca',
@@ -57,15 +63,30 @@ class CertprofileTracker(Tracker):
             content = f.read()
         return unicode(content)
 
+    def _format_mappings(self, mappings):
+        if mappings is None:
+            return None
+        else:
+            return unicode(json.dumps(mappings))
+
+    def check_mappings(self, tmpdir):
+        mappings = tmpdir.join('{}.mappings.json'.format(self.name))
+
+        command = self.make_retrieve_command(mappings_out=unicode(mappings))
+        command()
+
+        content = json.load(mappings)
+        nose.tools.assert_items_equal(content, self.mappings)
+
     def make_create_command(self, force=True):
         if not self.profile:
             raise RuntimeError('Tracker object without path to profile '
                                'cannot be used to create profile entry.')
 
-        return self.make_command('certprofile_import', self.name,
-                                 description=self.description,
-                                 ipacertprofilestoreissued=self.store,
-                                 file=self.profile)
+        return self.make_command(
+            'certprofile_import', self.name, description=self.description,
+            ipacertprofilestoreissued=self.store, file=self.profile,
+            mappings_file=self._format_mappings(self.mappings))
 
     def check_create(self, result):
         assert_deepequal(dict(
@@ -82,6 +103,7 @@ class CertprofileTracker(Tracker):
             ipacertprofilestoreissued=[unicode(self.store).upper()],
             objectclass=objectclasses.certprofile
         )
+
         self.exists = True
 
     def make_delete_command(self):
@@ -126,6 +148,15 @@ class CertprofileTracker(Tracker):
             result=[expected]
         ), result)
 
+    def update(self, updates, expected_updates=None, mappings=None):
+        passed_updates = updates.copy()
+        if mappings is not None:
+            self.mappings = mappings
+            passed_updates['mappings_file'] = self._format_mappings(mappings)
+
+        return super(CertprofileTracker, self).update(
+            passed_updates, expected_updates)
+
     def make_update_command(self, updates):
         return self.make_command('certprofile_mod', self.name, **updates)
 
@@ -133,5 +164,7 @@ class CertprofileTracker(Tracker):
         assert_deepequal(dict(
             value=self.name,
             summary=u'Modified Certificate Profile "{}"'.format(self.name),
-            result=self.filter_attrs(self.update_keys | set(extra_keys))
+            result=self.filter_attrs(
+                self.update_keys
+                | (set(extra_keys) - self.ignore_extra_keys))
         ), result)
-- 
2.5.5

-- 
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