Hello,

The attached patches improve upon my last patchset to:

0013: Add support for generating a full script that makes a CSR, rather than just a config, and use that support to automate the full flow from script generation through cert issuance Usage note: the UI for this could probably use work. I currently have the --helper-args param that allows additional data to be passed to the helper. Commonly this would be something like: Certutil: --helper-args '-d /path/to/nss/db' (precreated with certutil -N -d /path/to/nss/db) Openssl: --helper-args 'd /path/to/keyfile' (precreated with openssl genrsa -out /path/to/keyfile)
See the commit message for a full command line.

0014: Allow the feature to be used by non-admin users

0015: Improve error handling by reporting a nice message if the mapping rules are broken, or if the data required to generate the subject DN is missing

These improvements may make it easier to test the other patches.

Thanks,
Ben
From 4108cc1e1dd1751993c3ac40f5f5dfbe18e03ca2 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Fri, 5 Aug 2016 09:29:13 -0400
Subject: [PATCH 13/15] Automate full cert request flow

Adds `cert-build` command that pulls down a config with
`cert-get-requestdata`, then uses it to build a CSR, and submits the CSR
with `cert-request`. To enable this, the format of the returned configs
has been changed to a bash script, which produces the CSR when executed.

Example usage:
$ ipa cert-build --principal blipton --profile-id userCert --helper certutil \
  --helper-args '-d /tmp/certs'

https://fedorahosted.org/freeipa/ticket/4899
---
 API.txt                                       | 21 ++++++++-
 install/share/csrtemplates/certutil_base.tmpl | 12 +++++-
 install/share/csrtemplates/openssl_base.tmpl  | 19 ++++++++-
 ipaclient/plugins/cert.py                     | 61 ++++++++++++++++++++++++++-
 ipaclient/plugins/certmapping.py              | 20 ++++++---
 ipaserver/plugins/cert.py                     | 29 +++++++++++++
 ipaserver/plugins/certmapping.py              | 14 +++---
 7 files changed, 161 insertions(+), 15 deletions(-)

diff --git a/API.txt b/API.txt
index d724cad0d2898102b358229e5699c6bbc9d94702..8cbbf4cf4fd70f19062b8b491ed2ef452754d981 100644
--- a/API.txt
+++ b/API.txt
@@ -723,6 +723,21 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: cert_build/1
+args: 0,10,3
+option: Flag('add', autofill=True, default=False)
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa')
+option: Str('helper?')
+option: Str('helper_args?')
+option: Principal('principal')
+option: Str('profile_id?')
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('request_type', autofill=True, default=u'pkcs10')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: cert_find/1
 args: 1,29,4
 arg: Str('criteria?')
@@ -760,8 +775,9 @@ 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')
+args: 0,5,1
+option: Str('helper')
+option: Str('out?')
 option: Principal('principal')
 option: Str('profile_id')
 option: Str('version?')
@@ -6455,6 +6471,7 @@ default: caacl_remove_service/1
 default: caacl_remove_user/1
 default: caacl_show/1
 default: cert/1
+default: cert_build/1
 default: cert_find/1
 default: cert_get_requestdata/1
 default: cert_remove_hold/1
diff --git a/install/share/csrtemplates/certutil_base.tmpl b/install/share/csrtemplates/certutil_base.tmpl
index d57273c04acede24af054c16e2128a8405bd2460..6c6425fc02867a3bc6d39313a2e07601819a7708 100644
--- a/install/share/csrtemplates/certutil_base.tmpl
+++ b/install/share/csrtemplates/certutil_base.tmpl
@@ -1,4 +1,14 @@
 {% raw -%}
 {% import "ipa_macros.tmpl" as ipa -%}
 {%- endraw %}
-certutil -R -a {{ options|join(' ') }}
+#!/bin/bash -e
+
+if [[ $# -lt 1 ]]; then
+echo "Usage: $0 <outfile> [<any> <certutil> <args>]"
+echo "Called as: $0 $@"
+exit 1
+fi
+
+CSR="$1"
+shift
+certutil -R -a -z <(head -c 4096 /dev/urandom) -o "$CSR" {{ options|join(' ') }} "$@"
diff --git a/install/share/csrtemplates/openssl_base.tmpl b/install/share/csrtemplates/openssl_base.tmpl
index bdc58bd4afb2f55df800c19d691dd570883038de..597577bcd3366394f9cb0d1efa4b9dfe5790bc8f 100644
--- a/install/share/csrtemplates/openssl_base.tmpl
+++ b/install/share/csrtemplates/openssl_base.tmpl
@@ -2,6 +2,20 @@
 {% import "openssl_macros.tmpl" as openssl -%}
 {% import "ipa_macros.tmpl" as ipa -%}
 {%- endraw %}
+#!/bin/bash -e
+
+if [[ $# -ne 2 ]]; then
+echo "Usage: $0 <outfile> <keyfile>"
+echo "Called as: $0 $@"
+exit 1
+fi
+
+CONFIG="$(mktemp)"
+CSR="$1"
+shift
+
+echo \
+{% raw %}{% filter quote %}{% endraw -%}
 [ req ]
 prompt = no
 encrypt_key = no
@@ -15,4 +29,7 @@ encrypt_key = no
 req_extensions = {% call openssl.section() %}{{ rendered_extensions }}{% endcall %}
 {% endif %}
 {{ openssl.openssl_sections|join('\n\n') }}
-{%- endraw %}
+{% endfilter %}{%- endraw %} > "$CONFIG"
+
+openssl req -new -config "$CONFIG" -out "$CSR" -key $1
+rm "$CONFIG"
diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 1075972c43f08328762b97c5259b8cbb60bb545c..1b5f638e36acb2f2b58b566ddca7e107ae537263 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -19,7 +19,12 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from ipaclient.frontend import MethodOverride
+import os
+import shlex
+import subprocess
+import tempfile
+
+from ipaclient.frontend import CommandOverride, MethodOverride
 from ipalib import errors
 from ipalib import x509
 from ipalib import util
@@ -27,6 +32,11 @@ from ipalib.parameters import File, Flag, Str
 from ipalib.plugable import Registry
 from ipalib.text import _
 
+import six
+
+if six.PY3:
+    unicode = str
+
 register = Registry()
 
 
@@ -40,6 +50,55 @@ class cert_request(MethodOverride):
 
 
 @register(override=True, no_fail=True)
+class cert_build(CommandOverride):
+    def forward(self, *keys, **options):
+        try:
+            helper = options.pop('helper')
+        except KeyError:
+            raise errors.RequirementError(name='helper')
+
+        helper_args = options.pop('helper_args', u'')
+
+        scriptfile = tempfile.NamedTemporaryFile(delete=False)
+        scriptfile.close()
+        csrfile = tempfile.NamedTemporaryFile(delete=False)
+        csrfile.close()
+        csrfilename = csrfile.name
+
+        requestdata = self.api.Command.cert_get_requestdata(
+            profile_id=options.get('profile_id'),
+            principal=options.get('principal'),
+            out=unicode(scriptfile.name),
+            helper=helper)
+
+        helper_cmd = ['bash', '-e', scriptfile.name, csrfilename] + shlex.split(helper_args)
+
+        try:
+            subprocess.check_call(helper_cmd)
+        except subprocess.CalledProcessError:
+            raise errors.CertificateOperationError(
+                error=(
+                    _('Error running "%(cmd)s" to generate CSR') %
+                    {'cmd': ' '.join(helper_cmd)}))
+        finally:
+            os.remove(scriptfile.name)
+
+        try:
+            with open(csrfilename) as csrfile:
+                csr = csrfile.read()
+        except IOError:
+            raise errors.CertificateOperationError(
+                error=(_('Unable to read generated CSR file')))
+        if not csr:
+            raise errors.CertificateOperationError(
+                error=(_('Generated CSR was empty')))
+
+        rv = self.api.Command.cert_request(unicode(csr), **options)
+        os.remove(csrfilename)
+        return rv
+
+
+@register(override=True, no_fail=True)
 class cert_show(MethodOverride):
     def forward(self, *keys, **options):
         if 'out' in options:
diff --git a/ipaclient/plugins/certmapping.py b/ipaclient/plugins/certmapping.py
index 12d274e4c29c76a92e7cbbf644f0155aa7287b55..443e921ff379b0c80e208d3a6ba8f9685f7acc6f 100644
--- a/ipaclient/plugins/certmapping.py
+++ b/ipaclient/plugins/certmapping.py
@@ -3,6 +3,7 @@
 #
 
 from ipaclient.frontend import CommandOverride, Str
+from ipalib import util
 from ipalib.plugable import Registry
 from ipalib.text import _
 
@@ -20,10 +21,19 @@ Command override to display the produced CSR generation data
 @register(override=True, no_fail=True)
 class cert_get_requestdata(CommandOverride):
     has_output_params = (
-        Str('commandline',
-            label=_('Command to run'),
-        ),
-        Str('configfile',
-            label=_('Configuration file contents'),
+        Str('script',
+            label=_('Generation script'),
         ),
     )
+
+    def forward(self, *keys, **options):
+        if 'out' in options:
+            util.check_writable_file(options['out'])
+
+        result = super(cert_get_requestdata, self).forward(*keys, **options)
+
+        if 'out' in options and 'script' in result['result']:
+            with open(options['out'], 'wb') as f:
+                f.write(result['result'].pop('script'))
+
+        return result
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 06041d3083565e8d093b610473d6083111d406d2..f849f23caee6be0ccd5467ada0505a8f8cab1101 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -618,6 +618,35 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
             value=pkey_to_value(int(result['request_id']), kw),
         )
 
+@register()
+class cert_build(Create, BaseCertMethod):
+    __doc__ = _(
+        'Automatically construct a certificate signing request and submit it.')
+
+    obj_name = 'certreq'
+    attr_name = 'build'
+
+    takes_options = cert_request.takes_options + (
+        Str(
+            'helper?',
+            label=_('Name of CSR generation helper'),
+            doc=_('Name of tool (e.g. openssl, certutil) that will be used to'
+                  ' create CSR'),
+        ),
+        Str(
+            'helper_args?',
+            label=_('Extra args for CSR generation helper'),
+        ),
+    )
+
+    operation="build certificate"
+
+    def get_args(self):
+        # FIXME: the 'no_create' flag is ignored for positional arguments
+        for arg in super(cert_build, self).get_args():
+            if arg.name == 'request_id':
+                continue
+            yield arg
 
 @register()
 class cert_status(Retrieve, BaseCertMethod, VirtualCommand):
diff --git a/ipaserver/plugins/certmapping.py b/ipaserver/plugins/certmapping.py
index c93e84a8cbf085141940d8c7b5d6c34bf730b452..e33ee9f84d8cb5e6f5a17bb48d93d3984d7eab50 100644
--- a/ipaserver/plugins/certmapping.py
+++ b/ipaserver/plugins/certmapping.py
@@ -250,11 +250,14 @@ class cert_get_requestdata(Command):
             label=_('Profile ID'),
             doc=_('Certificate Profile to use'),
         ),
-        Str('format',
+        Str('helper',
             label=_('Name of CSR generation tool'),
             doc=_('Name of tool (e.g. openssl, certutil) that will be used to'
                   ' create CSR'),
         ),
+        Str('out?',
+            doc=_('Write CSR generation script to file'),
+        ),
     )
 
     has_output = (
@@ -268,7 +271,7 @@ class cert_get_requestdata(Command):
     def execute(self, **kw):
         principal = kw.get('principal')
         profile_id = kw.get('profile_id')
-        helper = kw.get('format')
+        helper = kw.get('helper')
 
         try:
             if principal.is_host:
@@ -347,7 +350,8 @@ class Formatter(object):
         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)
+        script = combined_template.render(**render_data)
+        return dict(script=script)
 
     def _wrap_rule(self, rule, rule_type):
         template = '{%% call ipa.%srule() %%}%s{%% endcall %%}' % (
@@ -383,7 +387,7 @@ class OpenSSLFormatter(Formatter):
         rendered = self._format(
             'openssl_base.tmpl',
             {'parameters': parameters, 'extensions': extensions}, render_data)
-        return dict(configfile=rendered)
+        return rendered
 
     def prepare_syntax_rule(self, syntax_rule, data_rules):
         """Overrides method to pull out whether rule is an extension or not."""
@@ -400,7 +404,7 @@ 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)
+        return rendered
 
 
 @register()
-- 
2.5.5

From 6c1c9760e8e3ffcc20439f0bc9a16eb80513ef87 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 1 Aug 2016 14:07:46 -0400
Subject: [PATCH 14/15] Add ACIs for mapping rules

This allows non-admin users to read those rules to use the certificate
mapping feature, and users with appropriate permissions can modify the
rules as well.

https://fedorahosted.org/freeipa/ticket/4899
---
 ACI.txt                          | 22 +++++++++++
 ipaserver/plugins/certmapping.py | 85 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 107 insertions(+)

diff --git a/ACI.txt b/ACI.txt
index fddd5987f48ee479d768e4202b5ac3f22ec0b110..8dfbdefdf9d64892acacfad96c9e6b2a0b90fb49 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -40,6 +40,20 @@ dn: cn=caacls,cn=ca,dc=ipa,dc=example
 aci: (targetattr = "cn || description || ipaenabledflag")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Modify CA ACL";allow (write) groupdn = "ldap:///cn=System: Modify CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=caacls,cn=ca,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || description || entryusn || hostcategory || ipacacategory || ipacertprofilecategory || ipaenabledflag || ipamemberca || ipamembercertprofile || ipauniqueid || member || memberhost || memberservice || memberuser || modifytimestamp || objectclass || servicecategory || usercategory")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Read CA ACLs";allow (compare,read,search) userdn = "ldap:///all";;)
+dn: dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertfieldmappingrule)")(version 3.0;acl "permission:System: Delete Certificate Profile Mappings";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Profile Mappings,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertfieldmappingrule)")(version 3.0;acl "permission:System: Import Certificate Profile Mappings";allow (add) groupdn = "ldap:///cn=System: Import Certificate Profile Mappings,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || entryusn || ipacertdatamapping || ipacertsyntaxmapping || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertfieldmappingrule)")(version 3.0;acl "permission:System: Read Certificate Field Mappings";allow (compare,read,search) userdn = "ldap:///all";;)
+dn: cn=mappingrulesets,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertmappingruleset)")(version 3.0;acl "permission:System: Create Certificate Mapping Rules";allow (add) groupdn = "ldap:///cn=System: Create Certificate Mapping Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=mappingrulesets,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertmappingruleset)")(version 3.0;acl "permission:System: Delete Certificate Mapping Rules";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Mapping Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=mappingrulesets,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || description || entryusn || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertmappingruleset)")(version 3.0;acl "permission:System: Read Certificate Mapping Rules";allow (compare,read,search) userdn = "ldap:///all";;)
+dn: cn=mappingrulesets,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || description")(targetfilter = "(objectclass=ipacertmappingruleset)")(version 3.0;acl "permission:System: Update Certificate Mapping Rules";allow (write) groupdn = "ldap:///cn=System: Update Certificate Mapping Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
@@ -48,6 +62,14 @@ dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
 aci: (targetattr = "cn || description || ipacertprofilestoreissued")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Modify Certificate Profile";allow (write) groupdn = "ldap:///cn=System: Modify Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || description || entryusn || ipacertprofilestoreissued || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Read Certificate Profiles";allow (compare,read,search) userdn = "ldap:///all";;)
+dn: dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacerttransformationrule)")(version 3.0;acl "permission:System: Create Certificate Transformation Rules";allow (add) groupdn = "ldap:///cn=System: Create Certificate Transformation Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacerttransformationrule)")(version 3.0;acl "permission:System: Delete Certificate Transformation Rules";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Transformation Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || entryusn || ipacerttransformationhelper || ipacerttransformationtemplate || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacerttransformationrule)")(version 3.0;acl "permission:System: Read Certificate Transformation Rules";allow (compare,read,search) userdn = "ldap:///all";;)
+dn: dc=ipa,dc=example
+aci: (targetattr = "cn || ipacerttransformationhelper || ipacerttransformationtemplate")(targetfilter = "(objectclass=ipacerttransformationrule)")(version 3.0;acl "permission:System: Update Certificate Transformation Rules";allow (write) groupdn = "ldap:///cn=System: Update Certificate Transformation Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=ipaconfig,cn=etc,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: cn=costemplates,cn=accounts,dc=ipa,dc=example
diff --git a/ipaserver/plugins/certmapping.py b/ipaserver/plugins/certmapping.py
index e33ee9f84d8cb5e6f5a17bb48d93d3984d7eab50..1d87d1f90de6cc8cb330176df49209dd85581170 100644
--- a/ipaserver/plugins/certmapping.py
+++ b/ipaserver/plugins/certmapping.py
@@ -75,6 +75,29 @@ class certfieldmappingrule(LDAPObject):
         ),
     )
 
+    permission_filter_objectclasses = ['ipacertfieldmappingrule']
+    managed_permissions = {
+        'System: Read Certificate Field Mappings': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn',
+                'ipacertsyntaxmapping',
+                'ipacertdatamapping',
+                'objectclass',
+            },
+        },
+        'System: Import Certificate Profile Mappings': {
+            'ipapermright': {'add'},
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Delete Certificate Profile Mappings': {
+            'ipapermright': {'delete'},
+            'default_privileges': {'CA Administrator'},
+        },
+    }
+
 
 @register()
 class certfieldmappingrule_add(LDAPCreate):
@@ -138,6 +161,36 @@ class certmappingrule(LDAPObject):
         ),
     )
 
+    permission_filter_objectclasses = ['ipacertmappingruleset']
+    managed_permissions = {
+        'System: Read Certificate Mapping Rules': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn',
+                'description',
+                'objectclass',
+            },
+        },
+        'System: Create Certificate Mapping Rules': {
+            'ipapermright': {'add'},
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Update Certificate Mapping Rules': {
+            'ipapermright': {'write'},
+            'default_privileges': {'CA Administrator'},
+            'ipapermdefaultattr': {
+                'cn',
+                'description',
+            },
+        },
+        'System: Delete Certificate Mapping Rules': {
+            'ipapermright': {'delete'},
+            'default_privileges': {'CA Administrator'},
+        },
+    }
+
 
 @register()
 class certmappingrule_add(LDAPCreate):
@@ -209,6 +262,38 @@ class certtransformationrule(LDAPObject):
         ),
     )
 
+    permission_filter_objectclasses = ['ipacerttransformationrule']
+    managed_permissions = {
+        'System: Read Certificate Transformation Rules': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn',
+                'ipacerttransformationtemplate',
+                'ipacerttransformationhelper',
+                'objectclass',
+            },
+        },
+        'System: Create Certificate Transformation Rules': {
+            'ipapermright': {'add'},
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Update Certificate Transformation Rules': {
+            'ipapermright': {'write'},
+            'default_privileges': {'CA Administrator'},
+            'ipapermdefaultattr': {
+                'cn',
+                'ipacerttransformationtemplate',
+                'ipacerttransformationhelper',
+            },
+        },
+        'System: Delete Certificate Transformation Rules': {
+            'ipapermright': {'delete'},
+            'default_privileges': {'CA Administrator'},
+        },
+    }
+
 
 @register()
 class certtransformationrule_add(LDAPCreate):
-- 
2.5.5

From 572b171d287860ccd62e83630aa2f3a4204f9f24 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 1 Aug 2016 10:20:16 -0400
Subject: [PATCH 15/15] Improve error handling for certificate mapping

All calls to jinja2 will now raise an IPA error type if rendering does
not succeed, so that broken rules will not generate InternalErrors.
Additionally, if the rendering does not generate a subject DN (for
example if the wrong certificate profile for a principal is used), an
exception is raised, as the CSR creation will not succeed.

https://fedorahosted.org/freeipa/ticket/4899
---
 ipalib/errors.py                 |  9 ++++++++
 ipaserver/plugins/certmapping.py | 45 +++++++++++++++++++++++++++++++++-------
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/ipalib/errors.py b/ipalib/errors.py
index 7b4f15dd60ee80719195ba1b9b85d075b10bdf4f..ad46fc351ab7a0c0c3fd3dc4ba99de6f6b4cb0fa 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -1698,6 +1698,15 @@ class CertificateFormatError(CertificateError):
     format = _('Certificate format error: %(error)s')
 
 
+class CertificateMappingError(CertificateError):
+    """
+    **4035** Raised when a valid cert request config can not be generated
+    """
+
+    errno = 4303
+    format = _('%(reason)s')
+
+
 class MutuallyExclusiveError(ExecutionError):
     """
     **4303** Raised when an operation would result in setting two attributes which are mutually exlusive.
diff --git a/ipaserver/plugins/certmapping.py b/ipaserver/plugins/certmapping.py
index 1d87d1f90de6cc8cb330176df49209dd85581170..bda8ec1c72d61273bc74de2266f132b8708083ac 100644
--- a/ipaserver/plugins/certmapping.py
+++ b/ipaserver/plugins/certmapping.py
@@ -431,11 +431,22 @@ class Formatter(object):
     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)
+
+        try:
+            combined_template_source = base_template.render(**base_template_params)
+        except jinja2.UndefinedError:
+            raise errors.CertificateMappingError(reason=_(
+                'Template error when formatting certificate data'))
+
         self.backend.debug(
             'Formatting with template: %s' % combined_template_source)
         combined_template = self.jinja2.from_string(combined_template_source)
-        script = combined_template.render(**render_data)
+
+        try:
+            script = combined_template.render(**render_data)
+        except jinja2.UndefinedError:
+            raise errors.CertificateMappingError(reason=_(
+                'Template error when formatting certificate data'))
         return dict(script=script)
 
     def _wrap_rule(self, rule, rule_type):
@@ -450,8 +461,12 @@ class Formatter(object):
         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')
+        try:
+            rendered = template.render(datarules=data_rules)
+        except jinja2.UndefinedError:
+            raise errors.CertificateMappingError(reason=_(
+                'Template error when formatting certificate data'))
+        prepared_template = self._wrap_rule(rendered, 'syntax')
         return prepared_template
 
 
@@ -472,6 +487,12 @@ class OpenSSLFormatter(Formatter):
         rendered = self._format(
             'openssl_base.tmpl',
             {'parameters': parameters, 'extensions': extensions}, render_data)
+
+        if not 'distinguished_name =' in rendered['script']:
+            raise errors.CertificateMappingError(reason=_(
+                'Certificate subject could be generated. You may need to use a'
+                ' different certificate profile for this principal.'))
+
         return rendered
 
     def prepare_syntax_rule(self, syntax_rule, data_rules):
@@ -479,9 +500,13 @@ class OpenSSLFormatter(Formatter):
         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')
+        try:
+            is_extension = getattr(template.module, 'extension', False)
+            rendered = template.render(datarules=data_rules)
+        except jinja2.UndefinedError:
+            raise errors.CertificateMappingError(reason=_(
+                'Template error when formatting certificate data'))
+        prepared_template = self._wrap_rule(rendered, 'syntax')
         return self.SyntaxRule(prepared_template, is_extension)
 
 
@@ -489,6 +514,12 @@ class CertutilFormatter(Formatter):
     def format(self, syntax_rules, render_data):
         rendered = self._format(
             'certutil_base.tmpl', {'options': syntax_rules}, render_data)
+
+        if not ' -s ' in rendered['script']:
+            raise errors.CertificateMappingError(reason=_(
+                'Certificate subject could be generated. You may need to use a'
+                ' different certificate profile for this principal.'))
+
         return rendered
 
 
-- 
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