freeipa

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

Commit a26cf0d tests: Add tests for CSR autogeneration

13 files Authored by benlipton 2 years ago , Committed by jcholast 2 years ago ,
tests: Add tests for CSR autogeneration

This patch also contains some code changes to make the code easier to
test and to make the tests pass.

https://fedorahosted.org/freeipa/ticket/4899

Reviewed-By: Jan Cholasta <jcholast@redhat.com>

    
  1 @@ -13,7 +13,6 @@
  2   import jinja2.sandbox
  3   import six
  4   
  5 - from ipalib import api
  6   from ipalib import errors
  7   from ipalib.text import _
  8   from ipaplatform.paths import paths
  9 @@ -83,6 +82,11 @@
 10           self.passthrough_globals = {}
 11   
 12       def _define_passthrough(self, call):
 13 +         """Some macros are meant to be interpreted during the final render, not
 14 +         when data rules are interpolated into syntax rules. This method allows
 15 +         those macros to be registered so that calls to them are passed through
 16 +         to the prepared rule rather than interpreted.
 17 +         """
 18   
 19           def passthrough(caller):
 20               return u'{%% call %s() %%}%s{%% endcall %%}' % (call, caller())
 21 @@ -104,18 +108,20 @@
 22           :returns: jinja2.Template that can be rendered to produce the CSR data.
 23           """
 24           syntax_rules = []
 25 -         for description, syntax_rule, data_rules in rules:
 26 +         for field_mapping in rules:
 27               data_rules_prepared = [
 28 -                 self._prepare_data_rule(rule) for rule in data_rules]
 29 +                 self._prepare_data_rule(rule)
 30 +                 for rule in field_mapping.data_rules]
 31   
 32               data_sources = []
 33 -             for rule in data_rules:
 34 +             for rule in field_mapping.data_rules:
 35                   data_source = rule.options.get('data_source')
 36                   if data_source:
 37                       data_sources.append(data_source)
 38   
 39               syntax_rules.append(self._prepare_syntax_rule(
 40 -                 syntax_rule, data_rules_prepared, description, data_sources))
 41 +                 field_mapping.syntax_rule, data_rules_prepared,
 42 +                 field_mapping.description, data_sources))
 43   
 44           template_params = self._get_template_params(syntax_rules)
 45           base_template = self.jinja2.get_template(
 46 @@ -160,16 +166,19 @@
 47               syntax_rule.template, globals=self.passthrough_globals)
 48           is_required = syntax_rule.options.get('required', False)
 49           try:
 50 -             rendered = template.render(datarules=data_rules)
 51 +             prepared_template = template.render(datarules=data_rules)
 52           except jinja2.UndefinedError:
 53               logger.debug(traceback.format_exc())
 54               raise errors.CSRTemplateError(reason=_(
 55                   'Template error when formatting certificate data'))
 56   
 57 -         combinator = ' %s ' % syntax_rule.options.get(
 58 -             'data_source_combinator', 'or')
 59 -         condition = combinator.join(data_sources)
 60 -         prepared_template = self._wrap_conditional(rendered, condition)
 61 +         if data_sources:
 62 +             combinator = ' %s ' % syntax_rule.options.get(
 63 +                 'data_source_combinator', 'or')
 64 +             condition = combinator.join(data_sources)
 65 +             prepared_template = self._wrap_conditional(
 66 +                 prepared_template, condition)
 67 + 
 68           if is_required:
 69               prepared_template = self._wrap_required(
 70                   prepared_template, description)
 71 @@ -198,8 +207,8 @@
 72       SyntaxRule = collections.namedtuple(
 73           'SyntaxRule', ['template', 'is_extension'])
 74   
 75 -     def __init__(self):
 76 -         super(OpenSSLFormatter, self).__init__()
 77 +     def __init__(self, *args, **kwargs):
 78 +         super(OpenSSLFormatter, self).__init__(*args, **kwargs)
 79           self._define_passthrough('openssl.section')
 80   
 81       def _get_template_params(self, syntax_rules):
 82 @@ -226,17 +235,31 @@
 83           return {'options': syntax_rules}
 84   
 85   
 86 - # FieldMapping - representation of the rules needed to construct a complete
 87 - # certificate field.
 88 - # - description: str, a name or description of this field, to be used in
 89 - #   messages
 90 - # - syntax_rule: Rule, the rule defining the syntax of this field
 91 - # - data_rules: list of Rule, the rules that produce data to be stored in this
 92 - #   field
 93 - FieldMapping = collections.namedtuple(
 94 -     'FieldMapping', ['description', 'syntax_rule', 'data_rules'])
 95 - Rule = collections.namedtuple(
 96 -     'Rule', ['name', 'template', 'options'])
 97 + class FieldMapping(object):
 98 +     """Representation of the rules needed to construct a complete cert field.
 99 + 
100 +     Attributes:
101 +         description: str, a name or description of this field, to be used in
102 +             messages
103 +         syntax_rule: Rule, the rule defining the syntax of this field
104 +         data_rules: list of Rule, the rules that produce data to be stored in
105 +             this field
106 +     """
107 +     __slots__ = ['description', 'syntax_rule', 'data_rules']
108 + 
109 +     def __init__(self, description, syntax_rule, data_rules):
110 +         self.description = description
111 +         self.syntax_rule = syntax_rule
112 +         self.data_rules = data_rules
113 + 
114 + 
115 + class Rule(object):
116 +     __slots__ = ['name', 'template', 'options']
117 + 
118 +     def __init__(self, name, template, options):
119 +         self.name = name
120 +         self.template = template
121 +         self.options = options
122   
123   
124   class RuleProvider(object):
125 @@ -287,15 +310,22 @@
126                   options.update(ruleset['options'])
127               if 'options' in rule:
128                   options.update(rule['options'])
129 + 
130               self.rules[(rule_name, helper)] = Rule(
131                   rule_name, rule['template'], options)
132 + 
133           return self.rules[(rule_name, helper)]
134   
135       def rules_for_profile(self, profile_id, helper):
136           profile_path = os.path.join(self.csr_data_dir, 'profiles',
137                                       '%s.json' % profile_id)
138 -         with open(profile_path) as profile_file:
139 -             profile = json.load(profile_file)
140 +         try:
141 +             with open(profile_path) as profile_file:
142 +                 profile = json.load(profile_file)
143 +         except IOError:
144 +             raise errors.NotFound(
145 +                 reason=_('No CSR generation rules are defined for profile'
146 +                          ' %(profile_id)s') % {'profile_id': profile_id})
147   
148           field_mappings = []
149           for field in profile:
150 @@ -315,8 +345,7 @@
151       def __init__(self, rule_provider):
152           self.rule_provider = rule_provider
153   
154 -     def csr_script(self, principal, profile_id, helper):
155 -         config = api.Command.config_show()['result']
156 +     def csr_script(self, principal, config, profile_id, helper):
157           render_data = {'subject': principal, 'config': config}
158   
159           formatter = self.FORMATTERS[helper]()
 1 @@ -96,11 +96,12 @@
 2               raise errors.NotFound(
 3                   reason=_("The principal for this request doesn't exist."))
 4           principal_obj = principal_obj['result']
 5 +         config = api.Command.config_show()['result']
 6   
 7           generator = CSRGenerator(FileRuleProvider())
 8   
 9           script = generator.csr_script(
10 -             principal_obj, profile_id, helper)
11 +             principal_obj, config, profile_id, helper)
12   
13           result = {}
14           if 'out' in options:
 1 @@ -38,6 +38,7 @@
 2               "ipatests.test_cmdline",
 3               "ipatests.test_install",
 4               "ipatests.test_integration",
 5 +             "ipatests.test_ipaclient",
 6               "ipatests.test_ipalib",
 7               "ipatests.test_ipapython",
 8               "ipatests.test_ipaserver",
 9 @@ -51,6 +52,7 @@
10           package_data={
11               'ipatests.test_install': ['*.update'],
12               'ipatests.test_integration': ['scripts/*'],
13 +             'ipatests.test_ipaclient': ['data/*/*/*'],
14               'ipatests.test_ipalib': ['data/*'],
15               'ipatests.test_pkcs10': ['*.csr'],
16               "ipatests.test_ipaserver": ['data/*'],
1 @@ -0,0 +1,7 @@
2 + #
3 + # Copyright (C) 2016  FreeIPA Contributors see COPYING for license
4 + #
5 + 
6 + """
7 + Sub-package containing unit tests for `ipaclient` package.
8 + """
1 @@ -0,0 +1,8 @@
2 + [
3 +     {
4 +         "syntax": "basic",
5 +         "data": [
6 +             "options"
7 +         ]
8 +     }
9 + ]
 1 @@ -0,0 +1,12 @@
 2 + {
 3 +   "rules": [
 4 +     {
 5 +       "helper": "openssl",
 6 +       "template": "openssl_rule"
 7 +     },
 8 +     {
 9 +       "helper": "certutil",
10 +       "template": "certutil_rule"
11 +     }
12 +   ]
13 + }
 1 @@ -0,0 +1,18 @@
 2 + {
 3 +   "rules": [
 4 +     {
 5 +       "helper": "openssl",
 6 +       "template": "openssl_rule",
 7 +       "options": {
 8 +         "helper_option": true
 9 +       }
10 +     },
11 +     {
12 +       "helper": "certutil",
13 +       "template": "certutil_rule"
14 +     }
15 +   ],
16 +   "options": {
17 +     "global_option": true
18 +   }
19 + }
 1 @@ -0,0 +1,11 @@
 2 + #!/bin/bash -e
 3 + 
 4 + if [[ $# -lt 1 ]]; then
 5 + echo "Usage: $0 <outfile> [<any> <certutil> <args>]"
 6 + echo "Called as: $0 $@"
 7 + exit 1
 8 + fi
 9 + 
10 + CSR="$1"
11 + shift
12 + certutil -R -a -z <(head -c 4096 /dev/urandom) -o "$CSR" -s CN=machine.example.com,O=DOMAIN.EXAMPLE.COM --extSAN dns:machine.example.com "$@"
 1 @@ -0,0 +1,33 @@
 2 + #!/bin/bash -e
 3 + 
 4 + if [[ $# -ne 2 ]]; then
 5 + echo "Usage: $0 <outfile> <keyfile>"
 6 + echo "Called as: $0 $@"
 7 + exit 1
 8 + fi
 9 + 
10 + CONFIG="$(mktemp)"
11 + CSR="$1"
12 + shift
13 + 
14 + echo \
15 + '[ req ]
16 + prompt = no
17 + encrypt_key = no
18 + 
19 + distinguished_name = sec0
20 + req_extensions = sec2
21 + 
22 + [ sec0 ]
23 + O=DOMAIN.EXAMPLE.COM
24 + CN=machine.example.com
25 + 
26 + [ sec1 ]
27 + DNS = machine.example.com
28 + 
29 + [ sec2 ]
30 + subjectAltName = @sec1
31 + ' > "$CONFIG"
32 + 
33 + openssl req -new -config "$CONFIG" -out "$CSR" -key $1
34 + rm "$CONFIG"
 1 @@ -0,0 +1,11 @@
 2 + #!/bin/bash -e
 3 + 
 4 + if [[ $# -lt 1 ]]; then
 5 + echo "Usage: $0 <outfile> [<any> <certutil> <args>]"
 6 + echo "Called as: $0 $@"
 7 + exit 1
 8 + fi
 9 + 
10 + CSR="$1"
11 + shift
12 + certutil -R -a -z <(head -c 4096 /dev/urandom) -o "$CSR" -s CN=testuser,O=DOMAIN.EXAMPLE.COM --extSAN email:testuser@example.com "$@"
 1 @@ -0,0 +1,33 @@
 2 + #!/bin/bash -e
 3 + 
 4 + if [[ $# -ne 2 ]]; then
 5 + echo "Usage: $0 <outfile> <keyfile>"
 6 + echo "Called as: $0 $@"
 7 + exit 1
 8 + fi
 9 + 
10 + CONFIG="$(mktemp)"
11 + CSR="$1"
12 + shift
13 + 
14 + echo \
15 + '[ req ]
16 + prompt = no
17 + encrypt_key = no
18 + 
19 + distinguished_name = sec0
20 + req_extensions = sec2
21 + 
22 + [ sec0 ]
23 + O=DOMAIN.EXAMPLE.COM
24 + CN=testuser
25 + 
26 + [ sec1 ]
27 + email = testuser@example.com
28 + 
29 + [ sec2 ]
30 + subjectAltName = @sec1
31 + ' > "$CONFIG"
32 + 
33 + openssl req -new -config "$CONFIG" -out "$CSR" -key $1
34 + rm "$CONFIG"
1 @@ -0,0 +1,1 @@
2 + {{ options|join(";") }}
  1 @@ -0,0 +1,298 @@
  2 + #
  3 + # Copyright (C) 2016  FreeIPA Contributors see COPYING for license
  4 + #
  5 + 
  6 + import os
  7 + import pytest
  8 + 
  9 + from ipaclient import csrgen
 10 + from ipalib import errors
 11 + 
 12 + BASE_DIR = os.path.dirname(__file__)
 13 + CSR_DATA_DIR = os.path.join(BASE_DIR, 'data', 'test_csrgen')
 14 + 
 15 + 
 16 + @pytest.fixture
 17 + def formatter():
 18 +     return csrgen.Formatter(csr_data_dir=CSR_DATA_DIR)
 19 + 
 20 + 
 21 + @pytest.fixture
 22 + def rule_provider():
 23 +     return csrgen.FileRuleProvider(csr_data_dir=CSR_DATA_DIR)
 24 + 
 25 + 
 26 + @pytest.fixture
 27 + def generator():
 28 +     return csrgen.CSRGenerator(csrgen.FileRuleProvider())
 29 + 
 30 + 
 31 + class StubRuleProvider(csrgen.RuleProvider):
 32 +     def __init__(self):
 33 +         self.syntax_rule = csrgen.Rule(
 34 +             'syntax', '{{datarules|join(",")}}', {})
 35 +         self.data_rule = csrgen.Rule('data', 'data_template', {})
 36 +         self.field_mapping = csrgen.FieldMapping(
 37 +             'example', self.syntax_rule, [self.data_rule])
 38 +         self.rules = [self.field_mapping]
 39 + 
 40 +     def rules_for_profile(self, profile_id, helper):
 41 +         return self.rules
 42 + 
 43 + 
 44 + class IdentityFormatter(csrgen.Formatter):
 45 +     base_template_name = 'identity_base.tmpl'
 46 + 
 47 +     def __init__(self):
 48 +         super(IdentityFormatter, self).__init__(csr_data_dir=CSR_DATA_DIR)
 49 + 
 50 +     def _get_template_params(self, syntax_rules):
 51 +         return {'options': syntax_rules}
 52 + 
 53 + 
 54 + class IdentityCSRGenerator(csrgen.CSRGenerator):
 55 +     FORMATTERS = {'identity': IdentityFormatter}
 56 + 
 57 + 
 58 + class test_Formatter(object):
 59 +     def test_prepare_data_rule_with_data_source(self, formatter):
 60 +         data_rule = csrgen.Rule('uid', '{{subject.uid.0}}',
 61 +                                 {'data_source': 'subject.uid.0'})
 62 +         prepared = formatter._prepare_data_rule(data_rule)
 63 +         assert prepared == '{% if subject.uid.0 %}{{subject.uid.0}}{% endif %}'
 64 + 
 65 +     def test_prepare_data_rule_no_data_source(self, formatter):
 66 +         """Not a normal case, but we should handle it anyway"""
 67 +         data_rule = csrgen.Rule('uid', 'static_text', {})
 68 +         prepared = formatter._prepare_data_rule(data_rule)
 69 +         assert prepared == 'static_text'
 70 + 
 71 +     def test_prepare_syntax_rule_with_data_sources(self, formatter):
 72 +         syntax_rule = csrgen.Rule(
 73 +             'example', '{{datarules|join(",")}}', {})
 74 +         data_rules = ['{{subject.field1}}', '{{subject.field2}}']
 75 +         data_sources = ['subject.field1', 'subject.field2']
 76 +         prepared = formatter._prepare_syntax_rule(
 77 +             syntax_rule, data_rules, 'example', data_sources)
 78 + 
 79 +         assert prepared == (
 80 +             '{% if subject.field1 or subject.field2 %}{{subject.field1}},'
 81 +             '{{subject.field2}}{% endif %}')
 82 + 
 83 +     def test_prepare_syntax_rule_with_combinator(self, formatter):
 84 +         syntax_rule = csrgen.Rule('example', '{{datarules|join(",")}}',
 85 +                                   {'data_source_combinator': 'and'})
 86 +         data_rules = ['{{subject.field1}}', '{{subject.field2}}']
 87 +         data_sources = ['subject.field1', 'subject.field2']
 88 +         prepared = formatter._prepare_syntax_rule(
 89 +             syntax_rule, data_rules, 'example', data_sources)
 90 + 
 91 +         assert prepared == (
 92 +             '{% if subject.field1 and subject.field2 %}{{subject.field1}},'
 93 +             '{{subject.field2}}{% endif %}')
 94 + 
 95 +     def test_prepare_syntax_rule_required(self, formatter):
 96 +         syntax_rule = csrgen.Rule('example', '{{datarules|join(",")}}',
 97 +                                   {'required': True})
 98 +         data_rules = ['{{subject.field1}}']
 99 +         data_sources = ['subject.field1']
100 +         prepared = formatter._prepare_syntax_rule(
101 +             syntax_rule, data_rules, 'example', data_sources)
102 + 
103 +         assert prepared == (
104 +             '{% filter required("example") %}{% if subject.field1 %}'
105 +             '{{subject.field1}}{% endif %}{% endfilter %}')
106 + 
107 +     def test_prepare_syntax_rule_passthrough(self, formatter):
108 +         """
109 +         Calls to macros defined as passthrough are still call tags in the final
110 +         template.
111 +         """
112 +         formatter._define_passthrough('example.macro')
113 + 
114 +         syntax_rule = csrgen.Rule(
115 +             'example',
116 +             '{% call example.macro() %}{{datarules|join(",")}}{% endcall %}',
117 +             {})
118 +         data_rules = ['{{subject.field1}}']
119 +         data_sources = ['subject.field1']
120 +         prepared = formatter._prepare_syntax_rule(
121 +             syntax_rule, data_rules, 'example', data_sources)
122 + 
123 +         assert prepared == (
124 +             '{% if subject.field1 %}{% call example.macro() %}'
125 +             '{{subject.field1}}{% endcall %}{% endif %}')
126 + 
127 +     def test_prepare_syntax_rule_no_data_sources(self, formatter):
128 +         """Not a normal case, but we should handle it anyway"""
129 +         syntax_rule = csrgen.Rule(
130 +             'example', '{{datarules|join(",")}}', {})
131 +         data_rules = ['rule1', 'rule2']
132 +         data_sources = []
133 +         prepared = formatter._prepare_syntax_rule(
134 +             syntax_rule, data_rules, 'example', data_sources)
135 + 
136 +         assert prepared == 'rule1,rule2'
137 + 
138 + 
139 + class test_FileRuleProvider(object):
140 +     def test_rule_basic(self, rule_provider):
141 +         rule_name = 'basic'
142 + 
143 +         rule1 = rule_provider._rule(rule_name, 'openssl')
144 +         rule2 = rule_provider._rule(rule_name, 'certutil')
145 + 
146 +         assert rule1.template == 'openssl_rule'
147 +         assert rule2.template == 'certutil_rule'
148 + 
149 +     def test_rule_global_options(self, rule_provider):
150 +         rule_name = 'options'
151 + 
152 +         rule1 = rule_provider._rule(rule_name, 'openssl')
153 +         rule2 = rule_provider._rule(rule_name, 'certutil')
154 + 
155 +         assert rule1.options['global_option'] is True
156 +         assert rule2.options['global_option'] is True
157 + 
158 +     def test_rule_helper_options(self, rule_provider):
159 +         rule_name = 'options'
160 + 
161 +         rule1 = rule_provider._rule(rule_name, 'openssl')
162 +         rule2 = rule_provider._rule(rule_name, 'certutil')
163 + 
164 +         assert rule1.options['helper_option'] is True
165 +         assert 'helper_option' not in rule2.options
166 + 
167 +     def test_rule_nosuchrule(self, rule_provider):
168 +         with pytest.raises(errors.NotFound):
169 +             rule_provider._rule('nosuchrule', 'openssl')
170 + 
171 +     def test_rule_nosuchhelper(self, rule_provider):
172 +         with pytest.raises(errors.EmptyResult):
173 +             rule_provider._rule('basic', 'nosuchhelper')
174 + 
175 +     def test_rules_for_profile_success(self, rule_provider):
176 +         rules = rule_provider.rules_for_profile('profile', 'certutil')
177 + 
178 +         assert len(rules) == 1
179 +         field_mapping = rules[0]
180 +         assert field_mapping.syntax_rule.name == 'basic'
181 +         assert len(field_mapping.data_rules) == 1
182 +         assert field_mapping.data_rules[0].name == 'options'
183 + 
184 +     def test_rules_for_profile_nosuchprofile(self, rule_provider):
185 +         with pytest.raises(errors.NotFound):
186 +             rule_provider.rules_for_profile('nosuchprofile', 'certutil')
187 + 
188 + 
189 + class test_CSRGenerator(object):
190 +     def test_userCert_OpenSSL(self, generator):
191 +         principal = {
192 +             'uid': ['testuser'],
193 +             'mail': ['testuser@example.com'],
194 +         }
195 +         config = {
196 +             'ipacertificatesubjectbase': [
197 +                 'O=DOMAIN.EXAMPLE.COM'
198 +             ],
199 +         }
200 + 
201 +         script = generator.csr_script(principal, config, 'userCert', 'openssl')
202 +         with open(os.path.join(
203 +                 CSR_DATA_DIR, 'scripts', 'userCert_openssl.sh')) as f:
204 +             expected_script = f.read()
205 +         assert script == expected_script
206 + 
207 +     def test_userCert_Certutil(self, generator):
208 +         principal = {
209 +             'uid': ['testuser'],
210 +             'mail': ['testuser@example.com'],
211 +         }
212 +         config = {
213 +             'ipacertificatesubjectbase': [
214 +                 'O=DOMAIN.EXAMPLE.COM'
215 +             ],
216 +         }
217 + 
218 +         script = generator.csr_script(
219 +             principal, config, 'userCert', 'certutil')
220 + 
221 +         with open(os.path.join(
222 +                 CSR_DATA_DIR, 'scripts', 'userCert_certutil.sh')) as f:
223 +             expected_script = f.read()
224 +         assert script == expected_script
225 + 
226 +     def test_caIPAserviceCert_OpenSSL(self, generator):
227 +         principal = {
228 +             'krbprincipalname': [
229 +                 'HTTP/machine.example.com@DOMAIN.EXAMPLE.COM'
230 +             ],
231 +         }
232 +         config = {
233 +             'ipacertificatesubjectbase': [
234 +                 'O=DOMAIN.EXAMPLE.COM'
235 +             ],
236 +         }
237 + 
238 +         script = generator.csr_script(
239 +             principal, config, 'caIPAserviceCert', 'openssl')
240 +         with open(os.path.join(
241 +                 CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_openssl.sh')) as f:
242 +             expected_script = f.read()
243 +         assert script == expected_script
244 + 
245 +     def test_caIPAserviceCert_Certutil(self, generator):
246 +         principal = {
247 +             'krbprincipalname': [
248 +                 'HTTP/machine.example.com@DOMAIN.EXAMPLE.COM'
249 +             ],
250 +         }
251 +         config = {
252 +             'ipacertificatesubjectbase': [
253 +                 'O=DOMAIN.EXAMPLE.COM'
254 +             ],
255 +         }
256 + 
257 +         script = generator.csr_script(
258 +             principal, config, 'caIPAserviceCert', 'certutil')
259 +         with open(os.path.join(
260 +                 CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_certutil.sh')) as f:
261 +             expected_script = f.read()
262 +         assert script == expected_script
263 + 
264 + 
265 + class test_rule_handling(object):
266 +     def test_optionalAttributeMissing(self, generator):
267 +         principal = {'uid': 'testuser'}
268 +         rule_provider = StubRuleProvider()
269 +         rule_provider.data_rule.template = '{{subject.mail}}'
270 +         rule_provider.data_rule.options = {'data_source': 'subject.mail'}
271 +         generator = IdentityCSRGenerator(rule_provider)
272 + 
273 +         script = generator.csr_script(
274 +             principal, {}, 'example', 'identity')
275 +         assert script == '\n'
276 + 
277 +     def test_twoDataRulesOneMissing(self, generator):
278 +         principal = {'uid': 'testuser'}
279 +         rule_provider = StubRuleProvider()
280 +         rule_provider.data_rule.template = '{{subject.mail}}'
281 +         rule_provider.data_rule.options = {'data_source': 'subject.mail'}
282 +         rule_provider.field_mapping.data_rules.append(csrgen.Rule(
283 +             'data2', '{{subject.uid}}', {'data_source': 'subject.uid'}))
284 +         generator = IdentityCSRGenerator(rule_provider)
285 + 
286 +         script = generator.csr_script(principal, {}, 'example', 'identity')
287 +         assert script == ',testuser\n'
288 + 
289 +     def test_requiredAttributeMissing(self):
290 +         principal = {'uid': 'testuser'}
291 +         rule_provider = StubRuleProvider()
292 +         rule_provider.data_rule.template = '{{subject.mail}}'
293 +         rule_provider.data_rule.options = {'data_source': 'subject.mail'}
294 +         rule_provider.syntax_rule.options = {'required': True}
295 +         generator = IdentityCSRGenerator(rule_provider)
296 + 
297 +         with pytest.raises(errors.CSRTemplateError):
298 +             _script = generator.csr_script(
299 +                 principal, {}, 'example', 'identity')