summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBen Lipton <blipton@redhat.com>2016-07-05 14:19:35 -0400
committerJan Cholasta <jcholast@redhat.com>2017-01-31 10:20:28 +0100
commit10ef5947860f5098182b1f95c08c1158e2da15f9 (patch)
tree360f7bb40f2eab2dcc2a1fa2c8460643c77524ec
parent87400cdec1054971f50f90a0c63f18ab045f3833 (diff)
downloadfreeipa-10ef5947860f5098182b1f95c08c1158e2da15f9.tar.gz
freeipa-10ef5947860f5098182b1f95c08c1158e2da15f9.tar.xz
freeipa-10ef5947860f5098182b1f95c08c1158e2da15f9.zip
csrgen: Add code to generate scripts that generate CSRs
Adds a library that uses jinja2 to format a script that, when run, will build a CSR. Also adds a CLI command, 'cert-get-requestdata', that uses this library and builds the script for a given principal. The rules are read from json files in /usr/share/ipa/csr, but the rule provider is a separate class so that it can be replaced easily. https://fedorahosted.org/freeipa/ticket/4899 Reviewed-By: Jan Cholasta <jcholast@redhat.com>
-rw-r--r--configure.ac1
-rw-r--r--freeipa.spec.in11
-rw-r--r--install/share/Makefile.am1
-rw-r--r--install/share/csr/templates/ipa_macros.tmpl42
-rw-r--r--install/share/csrgen/Makefile.am27
-rw-r--r--install/share/csrgen/templates/certutil_base.tmpl14
-rw-r--r--install/share/csrgen/templates/openssl_base.tmpl35
-rw-r--r--install/share/csrgen/templates/openssl_macros.tmpl29
-rw-r--r--ipaclient/csrgen.py319
-rw-r--r--ipaclient/plugins/csrgen.py114
-rw-r--r--ipaclient/setup.py1
-rw-r--r--ipalib/errors.py28
-rw-r--r--ipaplatform/base/paths.py1
13 files changed, 623 insertions, 0 deletions
diff --git a/configure.ac b/configure.ac
index 6cd3a8904..ff5f7b645 100644
--- a/configure.ac
+++ b/configure.ac
@@ -533,6 +533,7 @@ AC_CONFIG_FILES([
install/share/Makefile
install/share/advise/Makefile
install/share/advise/legacy/Makefile
+ install/share/csrgen/Makefile
install/share/profiles/Makefile
install/share/schema.d/Makefile
install/ui/Makefile
diff --git a/freeipa.spec.in b/freeipa.spec.in
index a7e05f360..ba2e29448 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -153,6 +153,7 @@ BuildRequires: python-sssdconfig
BuildRequires: python-nose
BuildRequires: python-paste
BuildRequires: systemd-python
+BuildRequires: python2-jinja2
%if 0%{?with_python3}
# FIXME: this depedency is missing - server will not work
@@ -190,6 +191,7 @@ BuildRequires: python3-libsss_nss_idmap
BuildRequires: python3-nose
BuildRequires: python3-paste
BuildRequires: python3-systemd
+BuildRequires: python3-jinja2
%endif # with_python3
%endif # with_lint
@@ -489,6 +491,7 @@ Requires: %{name}-client-common = %{version}-%{release}
Requires: %{name}-common = %{version}-%{release}
Requires: python2-ipalib = %{version}-%{release}
Requires: python-dns >= 1.15
+Requires: python2-jinja2
%description -n python2-ipaclient
IPA is an integrated solution to provide centrally managed Identity (users,
@@ -511,6 +514,7 @@ Requires: %{name}-client-common = %{version}-%{release}
Requires: %{name}-common = %{version}-%{release}
Requires: python3-ipalib = %{version}-%{release}
Requires: python3-dns >= 1.15
+Requires: python3-jinja2
%description -n python3-ipaclient
IPA is an integrated solution to provide centrally managed Identity (users,
@@ -1217,6 +1221,13 @@ fi
%{_usr}/share/ipa/advise/legacy/*.template
%dir %{_usr}/share/ipa/profiles
%{_usr}/share/ipa/profiles/*.cfg
+%dir %{_usr}/share/ipa/csrgen
+%dir %{_usr}/share/ipa/csrgen/templates
+%{_usr}/share/ipa/csrgen/templates/*.tmpl
+%dir %{_usr}/share/ipa/csrgen/profiles
+%{_usr}/share/ipa/csrgen/profiles/*.json
+%dir %{_usr}/share/ipa/csrgen/rules
+%{_usr}/share/ipa/csrgen/rules/*.json
%dir %{_usr}/share/ipa/html
%{_usr}/share/ipa/html/ffconfig.js
%{_usr}/share/ipa/html/ffconfig_page.js
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index 10de84d08..715912d8b 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -2,6 +2,7 @@ NULL =
SUBDIRS = \
advise \
+ csrgen \
profiles \
schema.d \
$(NULL)
diff --git a/install/share/csr/templates/ipa_macros.tmpl b/install/share/csr/templates/ipa_macros.tmpl
new file mode 100644
index 000000000..e790d4eb5
--- /dev/null
+++ b/install/share/csr/templates/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/csrgen/Makefile.am b/install/share/csrgen/Makefile.am
new file mode 100644
index 000000000..7b718cca1
--- /dev/null
+++ b/install/share/csrgen/Makefile.am
@@ -0,0 +1,27 @@
+NULL =
+
+profiledir = $(IPA_DATA_DIR)/csrgen/profiles
+profile_DATA = \
+ $(NULL)
+
+ruledir = $(IPA_DATA_DIR)/csrgen/rules
+rule_DATA = \
+ $(NULL)
+
+templatedir = $(IPA_DATA_DIR)/csrgen/templates
+template_DATA = \
+ templates/certutil_base.tmpl \
+ templates/openssl_base.tmpl \
+ templates/openssl_macros.tmpl \
+ templates/ipa_macros.tmpl \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(profile_DATA) \
+ $(rule_DATA) \
+ $(template_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/install/share/csrgen/templates/certutil_base.tmpl b/install/share/csrgen/templates/certutil_base.tmpl
new file mode 100644
index 000000000..6c6425fc0
--- /dev/null
+++ b/install/share/csrgen/templates/certutil_base.tmpl
@@ -0,0 +1,14 @@
+{% raw -%}
+{% import "ipa_macros.tmpl" as ipa -%}
+{%- endraw %}
+#!/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/csrgen/templates/openssl_base.tmpl b/install/share/csrgen/templates/openssl_base.tmpl
new file mode 100644
index 000000000..597577bcd
--- /dev/null
+++ b/install/share/csrgen/templates/openssl_base.tmpl
@@ -0,0 +1,35 @@
+{% raw -%}
+{% 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
+
+{{ 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') }}
+{% endfilter %}{%- endraw %} > "$CONFIG"
+
+openssl req -new -config "$CONFIG" -out "$CSR" -key $1
+rm "$CONFIG"
diff --git a/install/share/csrgen/templates/openssl_macros.tmpl b/install/share/csrgen/templates/openssl_macros.tmpl
new file mode 100644
index 000000000..d31b8fef5
--- /dev/null
+++ b/install/share/csrgen/templates/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 %}
diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py
new file mode 100644
index 000000000..0ffad7b28
--- /dev/null
+++ b/ipaclient/csrgen.py
@@ -0,0 +1,319 @@
+#
+# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
+#
+
+import collections
+import json
+import os.path
+import pipes
+import traceback
+
+import jinja2
+import jinja2.ext
+import jinja2.sandbox
+import six
+
+from ipalib import api
+from ipalib import errors
+from ipalib.text import _
+from ipaplatform.paths import paths
+from ipapython.ipa_log_manager import log_mgr
+
+if six.PY3:
+ unicode = str
+
+__doc__ = _("""
+Routines for constructing certificate signing requests using IPA data and
+stored templates.
+""")
+
+logger = log_mgr.get_logger(__name__)
+
+
+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 IPAExtension(jinja2.ext.Extension):
+ """Jinja2 extension providing useful features for CSR generation rules."""
+
+ def __init__(self, environment):
+ super(IPAExtension, self).__init__(environment)
+
+ environment.filters.update(
+ quote=self.quote,
+ required=self.required,
+ )
+
+ def quote(self, data):
+ return pipes.quote(data)
+
+ def required(self, data, name):
+ if not data:
+ raise errors.CSRTemplateError(
+ reason=_('Required CSR generation rule %(name)s is missing data') %
+ {'name': name})
+ return data
+
+
+class Formatter(object):
+ """
+ Class for processing a set of CSR generation rules into a template.
+
+ The template can be rendered with user and database data to produce a
+ script, which generates a CSR when run.
+
+ Subclasses of Formatter should set the value of base_template_name to the
+ filename of a base template with spaces for the processed rules.
+ Additionally, they should override the _get_template_params method to
+ produce the correct output for the base template.
+ """
+ base_template_name = None
+
+ def __init__(self, csr_data_dir=paths.CSR_DATA_DIR):
+ self.jinja2 = jinja2.sandbox.SandboxedEnvironment(
+ loader=jinja2.FileSystemLoader(
+ os.path.join(csr_data_dir, 'templates')),
+ extensions=[jinja2.ext.ExprStmtExtension, IPAExtension],
+ 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 build_template(self, rules):
+ """
+ Construct a template that can produce CSR generator strings.
+
+ :param rules: list of FieldMapping to use to populate the template.
+
+ :returns: jinja2.Template that can be rendered to produce the CSR data.
+ """
+ syntax_rules = []
+ for description, syntax_rule, data_rules in rules:
+ data_rules_prepared = [
+ self._prepare_data_rule(rule) for rule in data_rules]
+ syntax_rules.append(self._prepare_syntax_rule(
+ syntax_rule, data_rules_prepared, description))
+
+ template_params = self._get_template_params(syntax_rules)
+ base_template = self.jinja2.get_template(
+ self.base_template_name, globals=self.passthrough_globals)
+
+ try:
+ combined_template_source = base_template.render(**template_params)
+ except jinja2.UndefinedError:
+ logger.debug(traceback.format_exc())
+ raise errors.CSRTemplateError(reason=_(
+ 'Template error when formatting certificate data'))
+
+ logger.debug(
+ 'Formatting with template: %s' % combined_template_source)
+ combined_template = self.jinja2.from_string(combined_template_source)
+
+ return combined_template
+
+ def _wrap_rule(self, rule, rule_type):
+ template = '{%% call ipa.%srule() %%}%s{%% endcall %%}' % (
+ rule_type, rule)
+
+ return template
+
+ def _wrap_required(self, rule, description):
+ template = '{%% filter required("%s") %%}%s{%% endfilter %%}' % (
+ description, rule)
+
+ return template
+
+ def _prepare_data_rule(self, data_rule):
+ return self._wrap_rule(data_rule.template, 'data')
+
+ def _prepare_syntax_rule(self, syntax_rule, data_rules, description):
+ logger.debug('Syntax rule template: %s' % syntax_rule.template)
+ template = self.jinja2.from_string(
+ syntax_rule.template, globals=self.passthrough_globals)
+ is_required = syntax_rule.options.get('required', False)
+ try:
+ rendered = template.render(datarules=data_rules)
+ except jinja2.UndefinedError:
+ logger.debug(traceback.format_exc())
+ raise errors.CSRTemplateError(reason=_(
+ 'Template error when formatting certificate data'))
+
+ prepared_template = self._wrap_rule(rendered, 'syntax')
+ if is_required:
+ prepared_template = self._wrap_required(
+ prepared_template, description)
+
+ return prepared_template
+
+ def _get_template_params(self, syntax_rules):
+ """
+ Package the syntax rules into fields expected by the base template.
+
+ :param syntax_rules: list of prepared syntax rules to be included in
+ the template.
+
+ :returns: dict of values needed to render the base template.
+ """
+ raise NotImplementedError('Formatter class must be subclassed')
+
+
+class OpenSSLFormatter(Formatter):
+ """Formatter class supporting the openssl command-line tool."""
+
+ base_template_name = 'openssl_base.tmpl'
+
+ # Syntax rules are wrapped in this data structure, to keep track of whether
+ # each goes in the extension or the root section
+ SyntaxRule = collections.namedtuple(
+ 'SyntaxRule', ['template', 'is_extension'])
+
+ def __init__(self):
+ super(OpenSSLFormatter, self).__init__()
+ self._define_passthrough('openssl.section')
+
+ def _get_template_params(self, syntax_rules):
+ 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]
+
+ return {'parameters': parameters, 'extensions': extensions}
+
+ def _prepare_syntax_rule(self, syntax_rule, data_rules, description):
+ """Overrides method to pull out whether rule is an extension or not."""
+ prepared_template = super(OpenSSLFormatter, self)._prepare_syntax_rule(
+ syntax_rule, data_rules, description)
+ is_extension = syntax_rule.options.get('extension', False)
+ return self.SyntaxRule(prepared_template, is_extension)
+
+
+class CertutilFormatter(Formatter):
+ base_template_name = 'certutil_base.tmpl'
+
+ def _get_template_params(self, syntax_rules):
+ return {'options': syntax_rules}
+
+
+# FieldMapping - representation of the rules needed to construct a complete
+# certificate field.
+# - description: str, a name or description of this field, to be used in
+# messages
+# - syntax_rule: Rule, the rule defining the syntax of this field
+# - data_rules: list of Rule, the rules that produce data to be stored in this
+# field
+FieldMapping = collections.namedtuple(
+ 'FieldMapping', ['description', 'syntax_rule', 'data_rules'])
+Rule = collections.namedtuple(
+ 'Rule', ['name', 'template', 'options'])
+
+
+class RuleProvider(object):
+ def rules_for_profile(self, profile_id, helper):
+ """
+ Return the rules needed to build a CSR using the given profile.
+
+ :param profile_id: str, name of the CSR generation profile to use
+ :param helper: str, name of tool (e.g. openssl, certutil) that will be
+ used to create CSR
+
+ :returns: list of FieldMapping, filled out with the appropriate rules
+ """
+ raise NotImplementedError('RuleProvider class must be subclassed')
+
+
+class FileRuleProvider(RuleProvider):
+ def __init__(self, csr_data_dir=paths.CSR_DATA_DIR):
+ self.rules = {}
+ self.csr_data_dir = csr_data_dir
+
+ def _rule(self, rule_name, helper):
+ if (rule_name, helper) not in self.rules:
+ rule_path = os.path.join(self.csr_data_dir, 'rules',
+ '%s.json' % rule_name)
+ try:
+ with open(rule_path) as rule_file:
+ ruleset = json.load(rule_file)
+ except IOError:
+ raise errors.NotFound(
+ reason=_('Ruleset %(ruleset)s does not exist.') %
+ {'ruleset': rule_name})
+
+ matching_rules = [r for r in ruleset['rules']
+ if r['helper'] == helper]
+ if len(matching_rules) == 0:
+ raise errors.EmptyResult(
+ reason=_('No transformation in "%(ruleset)s" rule supports'
+ ' helper "%(helper)s"') %
+ {'ruleset': rule_name, 'helper': helper})
+ elif len(matching_rules) > 1:
+ raise errors.RedundantMappingRule(
+ ruleset=rule_name, helper=helper)
+ rule = matching_rules[0]
+
+ options = {}
+ if 'options' in ruleset:
+ options.update(ruleset['options'])
+ if 'options' in rule:
+ options.update(rule['options'])
+ self.rules[(rule_name, helper)] = Rule(
+ rule_name, rule['template'], options)
+ return self.rules[(rule_name, helper)]
+
+ def rules_for_profile(self, profile_id, helper):
+ profile_path = os.path.join(self.csr_data_dir, 'profiles',
+ '%s.json' % profile_id)
+ with open(profile_path) as profile_file:
+ profile = json.load(profile_file)
+
+ field_mappings = []
+ for field in profile:
+ syntax_rule = self._rule(field['syntax'], helper)
+ data_rules = [self._rule(name, helper) for name in field['data']]
+ field_mappings.append(FieldMapping(
+ syntax_rule.name, syntax_rule, data_rules))
+ return field_mappings
+
+
+class CSRGenerator(object):
+ FORMATTERS = {
+ 'openssl': OpenSSLFormatter,
+ 'certutil': CertutilFormatter,
+ }
+
+ def __init__(self, rule_provider):
+ self.rule_provider = rule_provider
+
+ def csr_script(self, principal, profile_id, helper):
+ config = api.Command.config_show()['result']
+ render_data = {'subject': principal, 'config': config}
+
+ formatter = self.FORMATTERS[helper]()
+ rules = self.rule_provider.rules_for_profile(profile_id, helper)
+ template = formatter.build_template(rules)
+
+ try:
+ script = template.render(render_data)
+ except jinja2.UndefinedError:
+ logger.debug(traceback.format_exc())
+ raise errors.CSRTemplateError(reason=_(
+ 'Template error when formatting certificate data'))
+
+ return script
diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py
new file mode 100644
index 000000000..0ad5fa1ff
--- /dev/null
+++ b/ipaclient/plugins/csrgen.py
@@ -0,0 +1,114 @@
+#
+# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
+#
+
+import six
+
+from ipaclient.csrgen import CSRGenerator, FileRuleProvider
+from ipalib import api
+from ipalib import errors
+from ipalib import output
+from ipalib import util
+from ipalib.frontend import Local, Str
+from ipalib.parameters import Principal
+from ipalib.plugable import Registry
+from ipalib.text import _
+
+if six.PY3:
+ unicode = str
+
+register = Registry()
+
+__doc__ = _("""
+Commands to build certificate requests automatically
+""")
+
+
+@register()
+class cert_get_requestdata(Local):
+ __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',
+ label=_('Profile ID'),
+ doc=_('CSR Generation Profile to use'),
+ ),
+ 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 = (
+ output.Output(
+ 'result',
+ type=dict,
+ doc=_('Dictionary mapping variable name to value'),
+ ),
+ )
+
+ has_output_params = (
+ Str(
+ 'script',
+ label=_('Generation script'),
+ )
+ )
+
+ def execute(self, *args, **options):
+ if 'out' in options:
+ util.check_writable_file(options['out'])
+
+ principal = options.get('principal')
+ profile_id = options.get('profile_id')
+ helper = options.get('helper')
+
+ if self.api.env.in_server:
+ backend = self.api.Backend.ldap2
+ else:
+ backend = self.api.Backend.rpcclient
+ if not backend.isconnected():
+ backend.connect()
+
+ 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']
+
+ generator = CSRGenerator(FileRuleProvider())
+
+ script = generator.csr_script(
+ principal_obj, profile_id, helper)
+
+ result = {}
+ if 'out' in options:
+ with open(options['out'], 'wb') as f:
+ f.write(script)
+ else:
+ result = dict(script=script)
+
+ return dict(
+ result=result
+ )
diff --git a/ipaclient/setup.py b/ipaclient/setup.py
index c413fc551..e7c80729f 100644
--- a/ipaclient/setup.py
+++ b/ipaclient/setup.py
@@ -47,6 +47,7 @@ if __name__ == '__main__':
"cryptography",
"ipalib",
"ipapython",
+ "jinja2",
"python-nss",
"python-yubico",
"pyusb",
diff --git a/ipalib/errors.py b/ipalib/errors.py
index 88707ac31..6aaca708a 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -1422,6 +1422,34 @@ class HTTPRequestError(RemoteRetrieveError):
format = _('Request failed with status %(status)s: %(reason)s')
+class RedundantMappingRule(SingleMatchExpected):
+ """
+ **4036** Raised when more than one rule in a CSR generation ruleset matches
+ a particular helper.
+
+ For example:
+
+ >>> raise RedundantMappingRule(ruleset='syntaxSubject', helper='certutil')
+ Traceback (most recent call last):
+ ...
+ RedundantMappingRule: Mapping ruleset "syntaxSubject" has more than one
+ rule for the certutil helper.
+ """
+
+ errno = 4036
+ format = _('Mapping ruleset "%(ruleset)s" has more than one rule for the'
+ ' %(helper)s helper')
+
+
+class CSRTemplateError(ExecutionError):
+ """
+ **4037** Raised when evaluation of a CSR generation template fails
+ """
+
+ errno = 4037
+ format = _('%(reason)s')
+
+
class BuiltinError(ExecutionError):
"""
**4100** Base class for builtin execution errors (*4100 - 4199*).
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 0ba64efdb..423478917 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -233,6 +233,7 @@ class BasePathNamespace(object):
SCHEMA_COMPAT_ULDIF = "/usr/share/ipa/schema_compat.uldif"
IPA_JS_PLUGINS_DIR = "/usr/share/ipa/ui/js/plugins"
UPDATES_DIR = "/usr/share/ipa/updates/"
+ CSR_DATA_DIR = "/usr/share/ipa/csrgen"
DICT_WORDS = "/usr/share/dict/words"
CACHE_IPA_SESSIONS = "/var/cache/ipa/sessions"
VAR_KERBEROS_KRB5KDC_DIR = "/var/kerberos/krb5kdc/"