freeipa

FreeIPA is an integrated Identity and Authentication solution for Linux/UNIX networked environments.  |  http://www.freeipa.org/

Commit 10ef594 csrgen: Add code to generate scripts that generate CSRs

13 files Authored by benlipton a year ago , Committed by jcholast a year ago ,
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>

    
1 @@ -533,6 +533,7 @@
2       install/share/Makefile
3       install/share/advise/Makefile
4       install/share/advise/legacy/Makefile
5 +     install/share/csrgen/Makefile
6       install/share/profiles/Makefile
7       install/share/schema.d/Makefile
8       install/ui/Makefile
 1 @@ -153,6 +153,7 @@
 2   BuildRequires:  python-nose
 3   BuildRequires:  python-paste
 4   BuildRequires:  systemd-python
 5 + BuildRequires:  python2-jinja2
 6   
 7   %if 0%{?with_python3}
 8   # FIXME: this depedency is missing - server will not work
 9 @@ -190,6 +191,7 @@
10   BuildRequires:  python3-nose
11   BuildRequires:  python3-paste
12   BuildRequires:  python3-systemd
13 + BuildRequires:  python3-jinja2
14   %endif # with_python3
15   %endif # with_lint
16   
17 @@ -489,6 +491,7 @@
18   Requires: %{name}-common = %{version}-%{release}
19   Requires: python2-ipalib = %{version}-%{release}
20   Requires: python-dns >= 1.15
21 + Requires: python2-jinja2
22   
23   %description -n python2-ipaclient
24   IPA is an integrated solution to provide centrally managed Identity (users,
25 @@ -511,6 +514,7 @@
26   Requires: %{name}-common = %{version}-%{release}
27   Requires: python3-ipalib = %{version}-%{release}
28   Requires: python3-dns >= 1.15
29 + Requires: python3-jinja2
30   
31   %description -n python3-ipaclient
32   IPA is an integrated solution to provide centrally managed Identity (users,
33 @@ -1217,6 +1221,13 @@
34   %{_usr}/share/ipa/advise/legacy/*.template
35   %dir %{_usr}/share/ipa/profiles
36   %{_usr}/share/ipa/profiles/*.cfg
37 + %dir %{_usr}/share/ipa/csrgen
38 + %dir %{_usr}/share/ipa/csrgen/templates
39 + %{_usr}/share/ipa/csrgen/templates/*.tmpl
40 + %dir %{_usr}/share/ipa/csrgen/profiles
41 + %{_usr}/share/ipa/csrgen/profiles/*.json
42 + %dir %{_usr}/share/ipa/csrgen/rules
43 + %{_usr}/share/ipa/csrgen/rules/*.json
44   %dir %{_usr}/share/ipa/html
45   %{_usr}/share/ipa/html/ffconfig.js
46   %{_usr}/share/ipa/html/ffconfig_page.js
1 @@ -2,6 +2,7 @@
2   
3   SUBDIRS =  »       »       »       »       \
4   »       advise»       »       »       »       \
5 + »       csrgen»       »       »       »       \
6   »       profiles»       »       »       \
7   »       schema.d»       »       »       \
8   »       $(NULL)
 1 @@ -0,0 +1,42 @@
 2 + {% set rendersyntax = {} %}
 3 + 
 4 + {% set renderdata = {} %}
 5 + 
 6 + {# Wrapper for syntax rules. We render the contents of the rule into a
 7 + variable, so that if we find that none of the contained data rules rendered we
 8 + can suppress the whole syntax rule. That is, a syntax rule is rendered either
 9 + if no data rules are specified (unusual) or if at least one of the data rules
10 + rendered successfully. #}
11 + {% macro syntaxrule() -%}
12 + {% do rendersyntax.update(none=true, any=false) -%}
13 + {% set contents -%}
14 + {{ caller() -}}
15 + {% endset -%}
16 + {% if rendersyntax['none'] or rendersyntax['any'] -%}
17 + {{ contents -}}
18 + {% endif -%}
19 + {% endmacro %}
20 + 
21 + {# Wrapper for data rules. A data rule is rendered only when all of the data
22 + fields it contains have data available. #}
23 + {% macro datarule() -%}
24 + {% do rendersyntax.update(none=false) -%}
25 + {% do renderdata.update(all=true) -%}
26 + {% set contents -%}
27 + {{ caller() -}}
28 + {% endset -%}
29 + {% if renderdata['all'] -%}
30 + {% do rendersyntax.update(any=true) -%}
31 + {{ contents -}}
32 + {% endif -%}
33 + {% endmacro %}
34 + 
35 + {# Wrapper for fields in data rules. If any value wrapped by this macro
36 + produces an empty string, the entire data rule will be suppressed. #}
37 + {% macro datafield(value) -%}
38 + {% if value -%}
39 + {{ value -}}
40 + {% else -%}
41 + {% do renderdata.update(all=false) -%}
42 + {% endif -%}
43 + {% endmacro %}
 1 @@ -0,0 +1,27 @@
 2 + NULL =
 3 + 
 4 + profiledir = $(IPA_DATA_DIR)/csrgen/profiles
 5 + profile_DATA =»       »       »       »       \
 6 + »       $(NULL)
 7 + 
 8 + ruledir = $(IPA_DATA_DIR)/csrgen/rules
 9 + rule_DATA =»       »       »       »       \
10 + »       $(NULL)
11 + 
12 + templatedir = $(IPA_DATA_DIR)/csrgen/templates
13 + template_DATA =»       »       »       \
14 + »       templates/certutil_base.tmpl»       \
15 + »       templates/openssl_base.tmpl»       \
16 + »       templates/openssl_macros.tmpl»       \
17 + »       templates/ipa_macros.tmpl»       \
18 + »       $(NULL)
19 + 
20 + EXTRA_DIST =»       »       »       »       \
21 + »       $(profile_DATA)»       »       »       \
22 + »       $(rule_DATA)»       »       »       \
23 + »       $(template_DATA)»       »       \
24 + »       $(NULL)
25 + 
26 + MAINTAINERCLEANFILES =»       »       »       \
27 + »       *~»       »       »       »       \
28 + »       Makefile.in
 1 @@ -0,0 +1,14 @@
 2 + {% raw -%}
 3 + {% import "ipa_macros.tmpl" as ipa -%}
 4 + {%- endraw %}
 5 + #!/bin/bash -e
 6 + 
 7 + if [[ $# -lt 1 ]]; then
 8 + echo "Usage: $0 <outfile> [<any> <certutil> <args>]"
 9 + echo "Called as: $0 $@"
10 + exit 1
11 + fi
12 + 
13 + CSR="$1"
14 + shift
15 + certutil -R -a -z <(head -c 4096 /dev/urandom) -o "$CSR" {{ options|join(' ') }} "$@"
 1 @@ -0,0 +1,35 @@
 2 + {% raw -%}
 3 + {% import "openssl_macros.tmpl" as openssl -%}
 4 + {% import "ipa_macros.tmpl" as ipa -%}
 5 + {%- endraw %}
 6 + #!/bin/bash -e
 7 + 
 8 + if [[ $# -ne 2 ]]; then
 9 + echo "Usage: $0 <outfile> <keyfile>"
10 + echo "Called as: $0 $@"
11 + exit 1
12 + fi
13 + 
14 + CONFIG="$(mktemp)"
15 + CSR="$1"
16 + shift
17 + 
18 + echo \
19 + {% raw %}{% filter quote %}{% endraw -%}
20 + [ req ]
21 + prompt = no
22 + encrypt_key = no
23 + 
24 + {{ parameters|join('\n') }}
25 + {% raw %}{% set rendered_extensions -%}{% endraw %}
26 + {{ extensions|join('\n') }}
27 + {% raw -%}
28 + {%- endset -%}
29 + {% if rendered_extensions -%}
30 + req_extensions = {% call openssl.section() %}{{ rendered_extensions }}{% endcall %}
31 + {% endif %}
32 + {{ openssl.openssl_sections|join('\n\n') }}
33 + {% endfilter %}{%- endraw %} > "$CONFIG"
34 + 
35 + openssl req -new -config "$CONFIG" -out "$CSR" -key $1
36 + rm "$CONFIG"
 1 @@ -0,0 +1,29 @@
 2 + {# List containing rendered sections to be included at end #}
 3 + {% set openssl_sections = [] %}
 4 + 
 5 + {#
 6 + List containing one entry for each section name allocated. Because of
 7 + scoping rules, we need to use a list so that it can be a "per-render global"
 8 + that gets updated in place. Real globals are shared by all templates with the
 9 + same environment, and variables defined in the macro don't persist after the
10 + macro invocation ends.
11 + #}
12 + {% set openssl_section_num = [] %}
13 + 
14 + {% macro section() -%}
15 + {% set name -%}
16 + sec{{ openssl_section_num|length -}}
17 + {% endset -%}
18 + {% do openssl_section_num.append('') -%}
19 + {% set contents %}{{ caller() }}{% endset -%}
20 + {% if contents -%}
21 + {% set sectiondata = formatsection(name, contents) -%}
22 + {% do openssl_sections.append(sectiondata) -%}
23 + {% endif -%}
24 + {{ name -}}
25 + {% endmacro %}
26 + 
27 + {% macro formatsection(name, contents) -%}
28 + [ {{ name }} ]
29 + {{ contents -}}
30 + {% endmacro %}
  1 @@ -0,0 +1,319 @@
  2 + #
  3 + # Copyright (C) 2016  FreeIPA Contributors see COPYING for license
  4 + #
  5 + 
  6 + import collections
  7 + import json
  8 + import os.path
  9 + import pipes
 10 + import traceback
 11 + 
 12 + import jinja2
 13 + import jinja2.ext
 14 + import jinja2.sandbox
 15 + import six
 16 + 
 17 + from ipalib import api
 18 + from ipalib import errors
 19 + from ipalib.text import _
 20 + from ipaplatform.paths import paths
 21 + from ipapython.ipa_log_manager import log_mgr
 22 + 
 23 + if six.PY3:
 24 +     unicode = str
 25 + 
 26 + __doc__ = _("""
 27 + Routines for constructing certificate signing requests using IPA data and
 28 + stored templates.
 29 + """)
 30 + 
 31 + logger = log_mgr.get_logger(__name__)
 32 + 
 33 + 
 34 + class IndexableUndefined(jinja2.Undefined):
 35 +     def __getitem__(self, key):
 36 +         return jinja2.Undefined(
 37 +             hint=self._undefined_hint, obj=self._undefined_obj,
 38 +             name=self._undefined_name, exc=self._undefined_exception)
 39 + 
 40 + 
 41 + class IPAExtension(jinja2.ext.Extension):
 42 +     """Jinja2 extension providing useful features for CSR generation rules."""
 43 + 
 44 +     def __init__(self, environment):
 45 +         super(IPAExtension, self).__init__(environment)
 46 + 
 47 +         environment.filters.update(
 48 +             quote=self.quote,
 49 +             required=self.required,
 50 +         )
 51 + 
 52 +     def quote(self, data):
 53 +         return pipes.quote(data)
 54 + 
 55 +     def required(self, data, name):
 56 +         if not data:
 57 +             raise errors.CSRTemplateError(
 58 +                 reason=_('Required CSR generation rule %(name)s is missing data') %
 59 +                 {'name': name})
 60 +         return data
 61 + 
 62 + 
 63 + class Formatter(object):
 64 +     """
 65 +     Class for processing a set of CSR generation rules into a template.
 66 + 
 67 +     The template can be rendered with user and database data to produce a
 68 +     script, which generates a CSR when run.
 69 + 
 70 +     Subclasses of Formatter should set the value of base_template_name to the
 71 +     filename of a base template with spaces for the processed rules.
 72 +     Additionally, they should override the _get_template_params method to
 73 +     produce the correct output for the base template.
 74 +     """
 75 +     base_template_name = None
 76 + 
 77 +     def __init__(self, csr_data_dir=paths.CSR_DATA_DIR):
 78 +         self.jinja2 = jinja2.sandbox.SandboxedEnvironment(
 79 +             loader=jinja2.FileSystemLoader(
 80 +                 os.path.join(csr_data_dir, 'templates')),
 81 +             extensions=[jinja2.ext.ExprStmtExtension, IPAExtension],
 82 +             keep_trailing_newline=True, undefined=IndexableUndefined)
 83 + 
 84 +         self.passthrough_globals = {}
 85 +         self._define_passthrough('ipa.syntaxrule')
 86 +         self._define_passthrough('ipa.datarule')
 87 + 
 88 +     def _define_passthrough(self, call):
 89 + 
 90 +         def passthrough(caller):
 91 +             return u'{%% call %s() %%}%s{%% endcall %%}' % (call, caller())
 92 + 
 93 +         parts = call.split('.')
 94 +         current_level = self.passthrough_globals
 95 +         for part in parts[:-1]:
 96 +             if part not in current_level:
 97 +                 current_level[part] = {}
 98 +             current_level = current_level[part]
 99 +         current_level[parts[-1]] = passthrough
100 + 
101 +     def build_template(self, rules):
102 +         """
103 +         Construct a template that can produce CSR generator strings.
104 + 
105 +         :param rules: list of FieldMapping to use to populate the template.
106 + 
107 +         :returns: jinja2.Template that can be rendered to produce the CSR data.
108 +         """
109 +         syntax_rules = []
110 +         for description, syntax_rule, data_rules in rules:
111 +             data_rules_prepared = [
112 +                 self._prepare_data_rule(rule) for rule in data_rules]
113 +             syntax_rules.append(self._prepare_syntax_rule(
114 +                 syntax_rule, data_rules_prepared, description))
115 + 
116 +         template_params = self._get_template_params(syntax_rules)
117 +         base_template = self.jinja2.get_template(
118 +             self.base_template_name, globals=self.passthrough_globals)
119 + 
120 +         try:
121 +             combined_template_source = base_template.render(**template_params)
122 +         except jinja2.UndefinedError:
123 +             logger.debug(traceback.format_exc())
124 +             raise errors.CSRTemplateError(reason=_(
125 +                 'Template error when formatting certificate data'))
126 + 
127 +         logger.debug(
128 +             'Formatting with template: %s' % combined_template_source)
129 +         combined_template = self.jinja2.from_string(combined_template_source)
130 + 
131 +         return combined_template
132 + 
133 +     def _wrap_rule(self, rule, rule_type):
134 +         template = '{%% call ipa.%srule() %%}%s{%% endcall %%}' % (
135 +             rule_type, rule)
136 + 
137 +         return template
138 + 
139 +     def _wrap_required(self, rule, description):
140 +         template = '{%% filter required("%s") %%}%s{%% endfilter %%}' % (
141 +             description, rule)
142 + 
143 +         return template
144 + 
145 +     def _prepare_data_rule(self, data_rule):
146 +         return self._wrap_rule(data_rule.template, 'data')
147 + 
148 +     def _prepare_syntax_rule(self, syntax_rule, data_rules, description):
149 +         logger.debug('Syntax rule template: %s' % syntax_rule.template)
150 +         template = self.jinja2.from_string(
151 +             syntax_rule.template, globals=self.passthrough_globals)
152 +         is_required = syntax_rule.options.get('required', False)
153 +         try:
154 +             rendered = template.render(datarules=data_rules)
155 +         except jinja2.UndefinedError:
156 +             logger.debug(traceback.format_exc())
157 +             raise errors.CSRTemplateError(reason=_(
158 +                 'Template error when formatting certificate data'))
159 + 
160 +         prepared_template = self._wrap_rule(rendered, 'syntax')
161 +         if is_required:
162 +             prepared_template = self._wrap_required(
163 +                 prepared_template, description)
164 + 
165 +         return prepared_template
166 + 
167 +     def _get_template_params(self, syntax_rules):
168 +         """
169 +         Package the syntax rules into fields expected by the base template.
170 + 
171 +         :param syntax_rules: list of prepared syntax rules to be included in
172 +             the template.
173 + 
174 +         :returns: dict of values needed to render the base template.
175 +         """
176 +         raise NotImplementedError('Formatter class must be subclassed')
177 + 
178 + 
179 + class OpenSSLFormatter(Formatter):
180 +     """Formatter class supporting the openssl command-line tool."""
181 + 
182 +     base_template_name = 'openssl_base.tmpl'
183 + 
184 +     # Syntax rules are wrapped in this data structure, to keep track of whether
185 +     # each goes in the extension or the root section
186 +     SyntaxRule = collections.namedtuple(
187 +         'SyntaxRule', ['template', 'is_extension'])
188 + 
189 +     def __init__(self):
190 +         super(OpenSSLFormatter, self).__init__()
191 +         self._define_passthrough('openssl.section')
192 + 
193 +     def _get_template_params(self, syntax_rules):
194 +         parameters = [rule.template for rule in syntax_rules
195 +                       if not rule.is_extension]
196 +         extensions = [rule.template for rule in syntax_rules
197 +                       if rule.is_extension]
198 + 
199 +         return {'parameters': parameters, 'extensions': extensions}
200 + 
201 +     def _prepare_syntax_rule(self, syntax_rule, data_rules, description):
202 +         """Overrides method to pull out whether rule is an extension or not."""
203 +         prepared_template = super(OpenSSLFormatter, self)._prepare_syntax_rule(
204 +             syntax_rule, data_rules, description)
205 +         is_extension = syntax_rule.options.get('extension', False)
206 +         return self.SyntaxRule(prepared_template, is_extension)
207 + 
208 + 
209 + class CertutilFormatter(Formatter):
210 +     base_template_name = 'certutil_base.tmpl'
211 + 
212 +     def _get_template_params(self, syntax_rules):
213 +         return {'options': syntax_rules}
214 + 
215 + 
216 + # FieldMapping - representation of the rules needed to construct a complete
217 + # certificate field.
218 + # - description: str, a name or description of this field, to be used in
219 + #   messages
220 + # - syntax_rule: Rule, the rule defining the syntax of this field
221 + # - data_rules: list of Rule, the rules that produce data to be stored in this
222 + #   field
223 + FieldMapping = collections.namedtuple(
224 +     'FieldMapping', ['description', 'syntax_rule', 'data_rules'])
225 + Rule = collections.namedtuple(
226 +     'Rule', ['name', 'template', 'options'])
227 + 
228 + 
229 + class RuleProvider(object):
230 +     def rules_for_profile(self, profile_id, helper):
231 +         """
232 +         Return the rules needed to build a CSR using the given profile.
233 + 
234 +         :param profile_id: str, name of the CSR generation profile to use
235 +         :param helper: str, name of tool (e.g. openssl, certutil) that will be
236 +             used to create CSR
237 + 
238 +         :returns: list of FieldMapping, filled out with the appropriate rules
239 +         """
240 +         raise NotImplementedError('RuleProvider class must be subclassed')
241 + 
242 + 
243 + class FileRuleProvider(RuleProvider):
244 +     def __init__(self, csr_data_dir=paths.CSR_DATA_DIR):
245 +         self.rules = {}
246 +         self.csr_data_dir = csr_data_dir
247 + 
248 +     def _rule(self, rule_name, helper):
249 +         if (rule_name, helper) not in self.rules:
250 +             rule_path = os.path.join(self.csr_data_dir, 'rules',
251 +                                      '%s.json' % rule_name)
252 +             try:
253 +                 with open(rule_path) as rule_file:
254 +                     ruleset = json.load(rule_file)
255 +             except IOError:
256 +                 raise errors.NotFound(
257 +                     reason=_('Ruleset %(ruleset)s does not exist.') %
258 +                     {'ruleset': rule_name})
259 + 
260 +             matching_rules = [r for r in ruleset['rules']
261 +                               if r['helper'] == helper]
262 +             if len(matching_rules) == 0:
263 +                 raise errors.EmptyResult(
264 +                     reason=_('No transformation in "%(ruleset)s" rule supports'
265 +                              ' helper "%(helper)s"') %
266 +                     {'ruleset': rule_name, 'helper': helper})
267 +             elif len(matching_rules) > 1:
268 +                 raise errors.RedundantMappingRule(
269 +                     ruleset=rule_name, helper=helper)
270 +             rule = matching_rules[0]
271 + 
272 +             options = {}
273 +             if 'options' in ruleset:
274 +                 options.update(ruleset['options'])
275 +             if 'options' in rule:
276 +                 options.update(rule['options'])
277 +             self.rules[(rule_name, helper)] = Rule(
278 +                 rule_name, rule['template'], options)
279 +         return self.rules[(rule_name, helper)]
280 + 
281 +     def rules_for_profile(self, profile_id, helper):
282 +         profile_path = os.path.join(self.csr_data_dir, 'profiles',
283 +                                     '%s.json' % profile_id)
284 +         with open(profile_path) as profile_file:
285 +             profile = json.load(profile_file)
286 + 
287 +         field_mappings = []
288 +         for field in profile:
289 +             syntax_rule = self._rule(field['syntax'], helper)
290 +             data_rules = [self._rule(name, helper) for name in field['data']]
291 +             field_mappings.append(FieldMapping(
292 +                 syntax_rule.name, syntax_rule, data_rules))
293 +         return field_mappings
294 + 
295 + 
296 + class CSRGenerator(object):
297 +     FORMATTERS = {
298 +         'openssl': OpenSSLFormatter,
299 +         'certutil': CertutilFormatter,
300 +     }
301 + 
302 +     def __init__(self, rule_provider):
303 +         self.rule_provider = rule_provider
304 + 
305 +     def csr_script(self, principal, profile_id, helper):
306 +         config = api.Command.config_show()['result']
307 +         render_data = {'subject': principal, 'config': config}
308 + 
309 +         formatter = self.FORMATTERS[helper]()
310 +         rules = self.rule_provider.rules_for_profile(profile_id, helper)
311 +         template = formatter.build_template(rules)
312 + 
313 +         try:
314 +             script = template.render(render_data)
315 +         except jinja2.UndefinedError:
316 +             logger.debug(traceback.format_exc())
317 +             raise errors.CSRTemplateError(reason=_(
318 +                 'Template error when formatting certificate data'))
319 + 
320 +         return script
  1 @@ -0,0 +1,114 @@
  2 + #
  3 + # Copyright (C) 2016  FreeIPA Contributors see COPYING for license
  4 + #
  5 + 
  6 + import six
  7 + 
  8 + from ipaclient.csrgen import CSRGenerator, FileRuleProvider
  9 + from ipalib import api
 10 + from ipalib import errors
 11 + from ipalib import output
 12 + from ipalib import util
 13 + from ipalib.frontend import Local, Str
 14 + from ipalib.parameters import Principal
 15 + from ipalib.plugable import Registry
 16 + from ipalib.text import _
 17 + 
 18 + if six.PY3:
 19 +     unicode = str
 20 + 
 21 + register = Registry()
 22 + 
 23 + __doc__ = _("""
 24 + Commands to build certificate requests automatically
 25 + """)
 26 + 
 27 + 
 28 + @register()
 29 + class cert_get_requestdata(Local):
 30 +     __doc__ = _('Gather data for a certificate signing request.')
 31 + 
 32 +     takes_options = (
 33 +         Principal(
 34 +             'principal',
 35 +             label=_('Principal'),
 36 +             doc=_('Principal for this certificate (e.g.'
 37 +                   ' HTTP/test.example.com)'),
 38 +         ),
 39 +         Str(
 40 +             'profile_id',
 41 +             label=_('Profile ID'),
 42 +             doc=_('CSR Generation Profile to use'),
 43 +         ),
 44 +         Str(
 45 +             'helper',
 46 +             label=_('Name of CSR generation tool'),
 47 +             doc=_('Name of tool (e.g. openssl, certutil) that will be used to'
 48 +                   ' create CSR'),
 49 +         ),
 50 +         Str(
 51 +             'out?',
 52 +             doc=_('Write CSR generation script to file'),
 53 +         ),
 54 +     )
 55 + 
 56 +     has_output = (
 57 +         output.Output(
 58 +             'result',
 59 +             type=dict,
 60 +             doc=_('Dictionary mapping variable name to value'),
 61 +         ),
 62 +     )
 63 + 
 64 +     has_output_params = (
 65 +         Str(
 66 +             'script',
 67 +             label=_('Generation script'),
 68 +         )
 69 +     )
 70 + 
 71 +     def execute(self, *args, **options):
 72 +         if 'out' in options:
 73 +             util.check_writable_file(options['out'])
 74 + 
 75 +         principal = options.get('principal')
 76 +         profile_id = options.get('profile_id')
 77 +         helper = options.get('helper')
 78 + 
 79 +         if self.api.env.in_server:
 80 +             backend = self.api.Backend.ldap2
 81 +         else:
 82 +             backend = self.api.Backend.rpcclient
 83 +         if not backend.isconnected():
 84 +             backend.connect()
 85 + 
 86 +         try:
 87 +             if principal.is_host:
 88 +                 principal_obj = api.Command.host_show(
 89 +                     principal.hostname, all=True)
 90 +             elif principal.is_service:
 91 +                 principal_obj = api.Command.service_show(
 92 +                     unicode(principal), all=True)
 93 +             elif principal.is_user:
 94 +                 principal_obj = api.Command.user_show(
 95 +                     principal.username, all=True)
 96 +         except errors.NotFound:
 97 +             raise errors.NotFound(
 98 +                 reason=_("The principal for this request doesn't exist."))
 99 +         principal_obj = principal_obj['result']
100 + 
101 +         generator = CSRGenerator(FileRuleProvider())
102 + 
103 +         script = generator.csr_script(
104 +             principal_obj, profile_id, helper)
105 + 
106 +         result = {}
107 +         if 'out' in options:
108 +             with open(options['out'], 'wb') as f:
109 +                 f.write(script)
110 +         else:
111 +             result = dict(script=script)
112 + 
113 +         return dict(
114 +             result=result
115 +         )
1 @@ -47,6 +47,7 @@
2               "cryptography",
3               "ipalib",
4               "ipapython",
5 +             "jinja2",
6               "python-nss",
7               "python-yubico",
8               "pyusb",
 1 @@ -1422,6 +1422,34 @@
 2       format = _('Request failed with status %(status)s: %(reason)s')
 3   
 4   
 5 + class RedundantMappingRule(SingleMatchExpected):
 6 +     """
 7 +     **4036** Raised when more than one rule in a CSR generation ruleset matches
 8 +     a particular helper.
 9 + 
10 +     For example:
11 + 
12 +     >>> raise RedundantMappingRule(ruleset='syntaxSubject', helper='certutil')
13 +     Traceback (most recent call last):
14 +       ...
15 +     RedundantMappingRule: Mapping ruleset "syntaxSubject" has more than one
16 +     rule for the certutil helper.
17 +     """
18 + 
19 +     errno = 4036
20 +     format = _('Mapping ruleset "%(ruleset)s" has more than one rule for the'
21 +                ' %(helper)s helper')
22 + 
23 + 
24 + class CSRTemplateError(ExecutionError):
25 +     """
26 +     **4037** Raised when evaluation of a CSR generation template fails
27 +     """
28 + 
29 +     errno = 4037
30 +     format = _('%(reason)s')
31 + 
32 + 
33   class BuiltinError(ExecutionError):
34       """
35       **4100** Base class for builtin execution errors (*4100 - 4199*).
1 @@ -233,6 +233,7 @@
2       SCHEMA_COMPAT_ULDIF = "/usr/share/ipa/schema_compat.uldif"
3       IPA_JS_PLUGINS_DIR = "/usr/share/ipa/ui/js/plugins"
4       UPDATES_DIR = "/usr/share/ipa/updates/"
5 +     CSR_DATA_DIR = "/usr/share/ipa/csrgen"
6       DICT_WORDS = "/usr/share/dict/words"
7       CACHE_IPA_SESSIONS = "/var/cache/ipa/sessions"
8       VAR_KERBEROS_KRB5KDC_DIR = "/var/kerberos/krb5kdc/"