From a26cf0d7910dd4c0a4da08682b4be8d3d94ba520 Mon Sep 17 00:00:00 2001 From: Ben Lipton Date: Thu, 8 Sep 2016 18:29:46 -0400 Subject: 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 --- ipatests/setup.py | 2 + ipatests/test_ipaclient/__init__.py | 7 + .../data/test_csrgen/profiles/profile.json | 8 + .../data/test_csrgen/rules/basic.json | 12 + .../data/test_csrgen/rules/options.json | 18 ++ .../scripts/caIPAserviceCert_certutil.sh | 11 + .../scripts/caIPAserviceCert_openssl.sh | 33 +++ .../data/test_csrgen/scripts/userCert_certutil.sh | 11 + .../data/test_csrgen/scripts/userCert_openssl.sh | 33 +++ .../data/test_csrgen/templates/identity_base.tmpl | 1 + ipatests/test_ipaclient/test_csrgen.py | 298 +++++++++++++++++++++ 11 files changed, 434 insertions(+) create mode 100644 ipatests/test_ipaclient/__init__.py create mode 100644 ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json create mode 100644 ipatests/test_ipaclient/data/test_csrgen/rules/basic.json create mode 100644 ipatests/test_ipaclient/data/test_csrgen/rules/options.json create mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh create mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh create mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh create mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh create mode 100644 ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl create mode 100644 ipatests/test_ipaclient/test_csrgen.py (limited to 'ipatests') diff --git a/ipatests/setup.py b/ipatests/setup.py index 1fb5e922f..e46e922fd 100644 --- a/ipatests/setup.py +++ b/ipatests/setup.py @@ -38,6 +38,7 @@ if __name__ == '__main__': "ipatests.test_cmdline", "ipatests.test_install", "ipatests.test_integration", + "ipatests.test_ipaclient", "ipatests.test_ipalib", "ipatests.test_ipapython", "ipatests.test_ipaserver", @@ -51,6 +52,7 @@ if __name__ == '__main__': package_data={ 'ipatests.test_install': ['*.update'], 'ipatests.test_integration': ['scripts/*'], + 'ipatests.test_ipaclient': ['data/*/*/*'], 'ipatests.test_ipalib': ['data/*'], 'ipatests.test_pkcs10': ['*.csr'], "ipatests.test_ipaserver": ['data/*'], diff --git a/ipatests/test_ipaclient/__init__.py b/ipatests/test_ipaclient/__init__.py new file mode 100644 index 000000000..0c428910c --- /dev/null +++ b/ipatests/test_ipaclient/__init__.py @@ -0,0 +1,7 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +""" +Sub-package containing unit tests for `ipaclient` package. +""" diff --git a/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json b/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json new file mode 100644 index 000000000..676f91bef --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/profiles/profile.json @@ -0,0 +1,8 @@ +[ + { + "syntax": "basic", + "data": [ + "options" + ] + } +] diff --git a/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json b/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json new file mode 100644 index 000000000..feba3e91e --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json @@ -0,0 +1,12 @@ +{ + "rules": [ + { + "helper": "openssl", + "template": "openssl_rule" + }, + { + "helper": "certutil", + "template": "certutil_rule" + } + ] +} diff --git a/ipatests/test_ipaclient/data/test_csrgen/rules/options.json b/ipatests/test_ipaclient/data/test_csrgen/rules/options.json new file mode 100644 index 000000000..111a6d80c --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/rules/options.json @@ -0,0 +1,18 @@ +{ + "rules": [ + { + "helper": "openssl", + "template": "openssl_rule", + "options": { + "helper_option": true + } + }, + { + "helper": "certutil", + "template": "certutil_rule" + } + ], + "options": { + "global_option": true + } +} diff --git a/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh b/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh new file mode 100644 index 000000000..74a704c2d --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh @@ -0,0 +1,11 @@ +#!/bin/bash -e + +if [[ $# -lt 1 ]]; then +echo "Usage: $0 [ ]" +echo "Called as: $0 $@" +exit 1 +fi + +CSR="$1" +shift +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 "$@" diff --git a/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh b/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh new file mode 100644 index 000000000..c621a69bc --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh @@ -0,0 +1,33 @@ +#!/bin/bash -e + +if [[ $# -ne 2 ]]; then +echo "Usage: $0 " +echo "Called as: $0 $@" +exit 1 +fi + +CONFIG="$(mktemp)" +CSR="$1" +shift + +echo \ +'[ req ] +prompt = no +encrypt_key = no + +distinguished_name = sec0 +req_extensions = sec2 + +[ sec0 ] +O=DOMAIN.EXAMPLE.COM +CN=machine.example.com + +[ sec1 ] +DNS = machine.example.com + +[ sec2 ] +subjectAltName = @sec1 +' > "$CONFIG" + +openssl req -new -config "$CONFIG" -out "$CSR" -key $1 +rm "$CONFIG" diff --git a/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh b/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh new file mode 100644 index 000000000..4aaeda07a --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh @@ -0,0 +1,11 @@ +#!/bin/bash -e + +if [[ $# -lt 1 ]]; then +echo "Usage: $0 [ ]" +echo "Called as: $0 $@" +exit 1 +fi + +CSR="$1" +shift +certutil -R -a -z <(head -c 4096 /dev/urandom) -o "$CSR" -s CN=testuser,O=DOMAIN.EXAMPLE.COM --extSAN email:testuser@example.com "$@" diff --git a/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh b/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh new file mode 100644 index 000000000..cdbe8a1fa --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh @@ -0,0 +1,33 @@ +#!/bin/bash -e + +if [[ $# -ne 2 ]]; then +echo "Usage: $0 " +echo "Called as: $0 $@" +exit 1 +fi + +CONFIG="$(mktemp)" +CSR="$1" +shift + +echo \ +'[ req ] +prompt = no +encrypt_key = no + +distinguished_name = sec0 +req_extensions = sec2 + +[ sec0 ] +O=DOMAIN.EXAMPLE.COM +CN=testuser + +[ sec1 ] +email = testuser@example.com + +[ sec2 ] +subjectAltName = @sec1 +' > "$CONFIG" + +openssl req -new -config "$CONFIG" -out "$CSR" -key $1 +rm "$CONFIG" diff --git a/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl b/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl new file mode 100644 index 000000000..79111ab68 --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/templates/identity_base.tmpl @@ -0,0 +1 @@ +{{ options|join(";") }} diff --git a/ipatests/test_ipaclient/test_csrgen.py b/ipatests/test_ipaclient/test_csrgen.py new file mode 100644 index 000000000..556f8e096 --- /dev/null +++ b/ipatests/test_ipaclient/test_csrgen.py @@ -0,0 +1,298 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +import os +import pytest + +from ipaclient import csrgen +from ipalib import errors + +BASE_DIR = os.path.dirname(__file__) +CSR_DATA_DIR = os.path.join(BASE_DIR, 'data', 'test_csrgen') + + +@pytest.fixture +def formatter(): + return csrgen.Formatter(csr_data_dir=CSR_DATA_DIR) + + +@pytest.fixture +def rule_provider(): + return csrgen.FileRuleProvider(csr_data_dir=CSR_DATA_DIR) + + +@pytest.fixture +def generator(): + return csrgen.CSRGenerator(csrgen.FileRuleProvider()) + + +class StubRuleProvider(csrgen.RuleProvider): + def __init__(self): + self.syntax_rule = csrgen.Rule( + 'syntax', '{{datarules|join(",")}}', {}) + self.data_rule = csrgen.Rule('data', 'data_template', {}) + self.field_mapping = csrgen.FieldMapping( + 'example', self.syntax_rule, [self.data_rule]) + self.rules = [self.field_mapping] + + def rules_for_profile(self, profile_id, helper): + return self.rules + + +class IdentityFormatter(csrgen.Formatter): + base_template_name = 'identity_base.tmpl' + + def __init__(self): + super(IdentityFormatter, self).__init__(csr_data_dir=CSR_DATA_DIR) + + def _get_template_params(self, syntax_rules): + return {'options': syntax_rules} + + +class IdentityCSRGenerator(csrgen.CSRGenerator): + FORMATTERS = {'identity': IdentityFormatter} + + +class test_Formatter(object): + def test_prepare_data_rule_with_data_source(self, formatter): + data_rule = csrgen.Rule('uid', '{{subject.uid.0}}', + {'data_source': 'subject.uid.0'}) + prepared = formatter._prepare_data_rule(data_rule) + assert prepared == '{% if subject.uid.0 %}{{subject.uid.0}}{% endif %}' + + def test_prepare_data_rule_no_data_source(self, formatter): + """Not a normal case, but we should handle it anyway""" + data_rule = csrgen.Rule('uid', 'static_text', {}) + prepared = formatter._prepare_data_rule(data_rule) + assert prepared == 'static_text' + + def test_prepare_syntax_rule_with_data_sources(self, formatter): + syntax_rule = csrgen.Rule( + 'example', '{{datarules|join(",")}}', {}) + data_rules = ['{{subject.field1}}', '{{subject.field2}}'] + data_sources = ['subject.field1', 'subject.field2'] + prepared = formatter._prepare_syntax_rule( + syntax_rule, data_rules, 'example', data_sources) + + assert prepared == ( + '{% if subject.field1 or subject.field2 %}{{subject.field1}},' + '{{subject.field2}}{% endif %}') + + def test_prepare_syntax_rule_with_combinator(self, formatter): + syntax_rule = csrgen.Rule('example', '{{datarules|join(",")}}', + {'data_source_combinator': 'and'}) + data_rules = ['{{subject.field1}}', '{{subject.field2}}'] + data_sources = ['subject.field1', 'subject.field2'] + prepared = formatter._prepare_syntax_rule( + syntax_rule, data_rules, 'example', data_sources) + + assert prepared == ( + '{% if subject.field1 and subject.field2 %}{{subject.field1}},' + '{{subject.field2}}{% endif %}') + + def test_prepare_syntax_rule_required(self, formatter): + syntax_rule = csrgen.Rule('example', '{{datarules|join(",")}}', + {'required': True}) + data_rules = ['{{subject.field1}}'] + data_sources = ['subject.field1'] + prepared = formatter._prepare_syntax_rule( + syntax_rule, data_rules, 'example', data_sources) + + assert prepared == ( + '{% filter required("example") %}{% if subject.field1 %}' + '{{subject.field1}}{% endif %}{% endfilter %}') + + def test_prepare_syntax_rule_passthrough(self, formatter): + """ + Calls to macros defined as passthrough are still call tags in the final + template. + """ + formatter._define_passthrough('example.macro') + + syntax_rule = csrgen.Rule( + 'example', + '{% call example.macro() %}{{datarules|join(",")}}{% endcall %}', + {}) + data_rules = ['{{subject.field1}}'] + data_sources = ['subject.field1'] + prepared = formatter._prepare_syntax_rule( + syntax_rule, data_rules, 'example', data_sources) + + assert prepared == ( + '{% if subject.field1 %}{% call example.macro() %}' + '{{subject.field1}}{% endcall %}{% endif %}') + + def test_prepare_syntax_rule_no_data_sources(self, formatter): + """Not a normal case, but we should handle it anyway""" + syntax_rule = csrgen.Rule( + 'example', '{{datarules|join(",")}}', {}) + data_rules = ['rule1', 'rule2'] + data_sources = [] + prepared = formatter._prepare_syntax_rule( + syntax_rule, data_rules, 'example', data_sources) + + assert prepared == 'rule1,rule2' + + +class test_FileRuleProvider(object): + def test_rule_basic(self, rule_provider): + rule_name = 'basic' + + rule1 = rule_provider._rule(rule_name, 'openssl') + rule2 = rule_provider._rule(rule_name, 'certutil') + + assert rule1.template == 'openssl_rule' + assert rule2.template == 'certutil_rule' + + def test_rule_global_options(self, rule_provider): + rule_name = 'options' + + rule1 = rule_provider._rule(rule_name, 'openssl') + rule2 = rule_provider._rule(rule_name, 'certutil') + + assert rule1.options['global_option'] is True + assert rule2.options['global_option'] is True + + def test_rule_helper_options(self, rule_provider): + rule_name = 'options' + + rule1 = rule_provider._rule(rule_name, 'openssl') + rule2 = rule_provider._rule(rule_name, 'certutil') + + assert rule1.options['helper_option'] is True + assert 'helper_option' not in rule2.options + + def test_rule_nosuchrule(self, rule_provider): + with pytest.raises(errors.NotFound): + rule_provider._rule('nosuchrule', 'openssl') + + def test_rule_nosuchhelper(self, rule_provider): + with pytest.raises(errors.EmptyResult): + rule_provider._rule('basic', 'nosuchhelper') + + def test_rules_for_profile_success(self, rule_provider): + rules = rule_provider.rules_for_profile('profile', 'certutil') + + assert len(rules) == 1 + field_mapping = rules[0] + assert field_mapping.syntax_rule.name == 'basic' + assert len(field_mapping.data_rules) == 1 + assert field_mapping.data_rules[0].name == 'options' + + def test_rules_for_profile_nosuchprofile(self, rule_provider): + with pytest.raises(errors.NotFound): + rule_provider.rules_for_profile('nosuchprofile', 'certutil') + + +class test_CSRGenerator(object): + def test_userCert_OpenSSL(self, generator): + principal = { + 'uid': ['testuser'], + 'mail': ['testuser@example.com'], + } + config = { + 'ipacertificatesubjectbase': [ + 'O=DOMAIN.EXAMPLE.COM' + ], + } + + script = generator.csr_script(principal, config, 'userCert', 'openssl') + with open(os.path.join( + CSR_DATA_DIR, 'scripts', 'userCert_openssl.sh')) as f: + expected_script = f.read() + assert script == expected_script + + def test_userCert_Certutil(self, generator): + principal = { + 'uid': ['testuser'], + 'mail': ['testuser@example.com'], + } + config = { + 'ipacertificatesubjectbase': [ + 'O=DOMAIN.EXAMPLE.COM' + ], + } + + script = generator.csr_script( + principal, config, 'userCert', 'certutil') + + with open(os.path.join( + CSR_DATA_DIR, 'scripts', 'userCert_certutil.sh')) as f: + expected_script = f.read() + assert script == expected_script + + def test_caIPAserviceCert_OpenSSL(self, generator): + principal = { + 'krbprincipalname': [ + 'HTTP/machine.example.com@DOMAIN.EXAMPLE.COM' + ], + } + config = { + 'ipacertificatesubjectbase': [ + 'O=DOMAIN.EXAMPLE.COM' + ], + } + + script = generator.csr_script( + principal, config, 'caIPAserviceCert', 'openssl') + with open(os.path.join( + CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_openssl.sh')) as f: + expected_script = f.read() + assert script == expected_script + + def test_caIPAserviceCert_Certutil(self, generator): + principal = { + 'krbprincipalname': [ + 'HTTP/machine.example.com@DOMAIN.EXAMPLE.COM' + ], + } + config = { + 'ipacertificatesubjectbase': [ + 'O=DOMAIN.EXAMPLE.COM' + ], + } + + script = generator.csr_script( + principal, config, 'caIPAserviceCert', 'certutil') + with open(os.path.join( + CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_certutil.sh')) as f: + expected_script = f.read() + assert script == expected_script + + +class test_rule_handling(object): + def test_optionalAttributeMissing(self, generator): + principal = {'uid': 'testuser'} + rule_provider = StubRuleProvider() + rule_provider.data_rule.template = '{{subject.mail}}' + rule_provider.data_rule.options = {'data_source': 'subject.mail'} + generator = IdentityCSRGenerator(rule_provider) + + script = generator.csr_script( + principal, {}, 'example', 'identity') + assert script == '\n' + + def test_twoDataRulesOneMissing(self, generator): + principal = {'uid': 'testuser'} + rule_provider = StubRuleProvider() + rule_provider.data_rule.template = '{{subject.mail}}' + rule_provider.data_rule.options = {'data_source': 'subject.mail'} + rule_provider.field_mapping.data_rules.append(csrgen.Rule( + 'data2', '{{subject.uid}}', {'data_source': 'subject.uid'})) + generator = IdentityCSRGenerator(rule_provider) + + script = generator.csr_script(principal, {}, 'example', 'identity') + assert script == ',testuser\n' + + def test_requiredAttributeMissing(self): + principal = {'uid': 'testuser'} + rule_provider = StubRuleProvider() + rule_provider.data_rule.template = '{{subject.mail}}' + rule_provider.data_rule.options = {'data_source': 'subject.mail'} + rule_provider.syntax_rule.options = {'required': True} + generator = IdentityCSRGenerator(rule_provider) + + with pytest.raises(errors.CSRTemplateError): + _script = generator.csr_script( + principal, {}, 'example', 'identity') -- cgit