URL: https://github.com/freeipa/freeipa/pull/397
Author: tiran
 Title: #397: Improve wheel building and provide ipaserver wheel for local 
testing
Action: synchronized

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/397/head:pr397
git checkout pr397
From 5420e9cfbe7803808b6e26d2dae64f2a6a50149a Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Tue, 21 Mar 2017 12:21:30 -0400
Subject: [PATCH 1/8] csrgen: Remove helper abstraction

All requests now use the OpenSSL formatter. However, we keep Formatter
a separate class so that it can be changed out for tests.

https://pagure.io/freeipa/issue/4899

Reviewed-By: Jan Cholasta <jchol...@redhat.com>
---
 ipaclient/csrgen.py                                | 71 ++++++----------
 ipaclient/csrgen/rules/dataDNS.json                | 13 +--
 ipaclient/csrgen/rules/dataEmail.json              | 13 +--
 ipaclient/csrgen/rules/dataHostCN.json             | 13 +--
 ipaclient/csrgen/rules/dataSubjectBase.json        | 13 +--
 ipaclient/csrgen/rules/dataUsernameCN.json         | 13 +--
 ipaclient/csrgen/rules/syntaxSAN.json              | 19 ++---
 ipaclient/csrgen/rules/syntaxSubject.json          | 13 +--
 ipaclient/csrgen/templates/certutil_base.tmpl      | 11 ---
 ipaclient/plugins/csrgen.py                        |  2 +-
 .../data/test_csrgen/configs/caIPAserviceCert.conf | 34 ++++++++
 .../data/test_csrgen/configs/userCert.conf         | 34 ++++++++
 .../data/test_csrgen/rules/basic.json              | 13 +--
 .../data/test_csrgen/rules/options.json            | 18 +---
 .../scripts/caIPAserviceCert_certutil.sh           | 11 ---
 .../scripts/caIPAserviceCert_openssl.sh            | 34 --------
 .../data/test_csrgen/scripts/userCert_certutil.sh  | 11 ---
 .../data/test_csrgen/scripts/userCert_openssl.sh   | 34 --------
 ipatests/test_ipaclient/test_csrgen.py             | 98 +++++-----------------
 19 files changed, 145 insertions(+), 323 deletions(-)
 delete mode 100644 ipaclient/csrgen/templates/certutil_base.tmpl
 create mode 100644 ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
 create mode 100644 ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
 delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh
 delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh
 delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh
 delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh

diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py
index 8fb0b32..8ca0722 100644
--- a/ipaclient/csrgen.py
+++ b/ipaclient/csrgen.py
@@ -244,13 +244,6 @@ def _prepare_syntax_rule(
         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}
-
-
 class FieldMapping(object):
     """Representation of the rules needed to construct a complete cert field.
 
@@ -279,13 +272,11 @@ def __init__(self, name, template, options):
 
 
 class RuleProvider(object):
-    def rules_for_profile(self, profile_id, helper):
+    def rules_for_profile(self, profile_id):
         """
         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
         """
@@ -321,40 +312,31 @@ def _open(self, subdir, filename):
             )
         )
 
-    def _rule(self, rule_name, helper):
-        if (rule_name, helper) not in self.rules:
+    def _rule(self, rule_name):
+        if rule_name not in self.rules:
             try:
                 with self._open('rules', '%s.json' % rule_name) as f:
-                    ruleset = json.load(f)
+                    ruleconf = json.load(f)
             except IOError:
                 raise errors.NotFound(
-                    reason=_('Ruleset %(ruleset)s does not exist.') %
-                    {'ruleset': rule_name})
+                    reason=_('No generation rule %(rulename)s found.') %
+                    {'rulename': rule_name})
 
-            matching_rules = [r for r in ruleset['rules']
-                              if r['helper'] == helper]
-            if len(matching_rules) == 0:
+            try:
+                rule = ruleconf['rule']
+            except KeyError:
                 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(
+                    reason=_('Generation rule "%(rulename)s" is missing the'
+                             ' "rule" key') % {'rulename': rule_name})
+
+            options = ruleconf.get('options', {})
+
+            self.rules[rule_name] = Rule(
                 rule_name, rule['template'], options)
 
-        return self.rules[(rule_name, helper)]
+        return self.rules[rule_name]
 
-    def rules_for_profile(self, profile_id, helper):
+    def rules_for_profile(self, profile_id):
         try:
             with self._open('profiles', '%s.json' % profile_id) as f:
                 profile = json.load(f)
@@ -365,28 +347,23 @@ def rules_for_profile(self, profile_id, helper):
 
         field_mappings = []
         for field in profile:
-            syntax_rule = self._rule(field['syntax'], helper)
-            data_rules = [self._rule(name, helper) for name in field['data']]
+            syntax_rule = self._rule(field['syntax'])
+            data_rules = [self._rule(name) 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):
+    def __init__(self, rule_provider, formatter_class=OpenSSLFormatter):
         self.rule_provider = rule_provider
+        self.formatter = formatter_class()
 
-    def csr_script(self, principal, config, profile_id, helper):
+    def csr_script(self, principal, config, profile_id):
         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)
+        rules = self.rule_provider.rules_for_profile(profile_id)
+        template = self.formatter.build_template(rules)
 
         try:
             script = template.render(render_data)
diff --git a/ipaclient/csrgen/rules/dataDNS.json b/ipaclient/csrgen/rules/dataDNS.json
index 2663f11..a79a3d7 100644
--- a/ipaclient/csrgen/rules/dataDNS.json
+++ b/ipaclient/csrgen/rules/dataDNS.json
@@ -1,14 +1,7 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "DNS = {{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}"
-    },
-    {
-      "helper": "certutil",
-      "template": "dns:{{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]|quote}}"
-    }
-  ],
+  "rule": {
+    "template": "DNS = {{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}"
+  },
   "options": {
     "data_source": "subject.krbprincipalname.0.partition('/')[2].partition('@')[0]"
   }
diff --git a/ipaclient/csrgen/rules/dataEmail.json b/ipaclient/csrgen/rules/dataEmail.json
index 2eae9fb..4be6cec 100644
--- a/ipaclient/csrgen/rules/dataEmail.json
+++ b/ipaclient/csrgen/rules/dataEmail.json
@@ -1,14 +1,7 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "email = {{subject.mail.0}}"
-    },
-    {
-      "helper": "certutil",
-      "template": "email:{{subject.mail.0|quote}}"
-    }
-  ],
+  "rule": {
+    "template": "email = {{subject.mail.0}}"
+  },
   "options": {
     "data_source": "subject.mail.0"
   }
diff --git a/ipaclient/csrgen/rules/dataHostCN.json b/ipaclient/csrgen/rules/dataHostCN.json
index 5c415bb..f30c50f 100644
--- a/ipaclient/csrgen/rules/dataHostCN.json
+++ b/ipaclient/csrgen/rules/dataHostCN.json
@@ -1,14 +1,7 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "CN={{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}"
-    },
-    {
-      "helper": "certutil",
-      "template": "CN={{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]|quote}}"
-    }
-  ],
+  "rule": {
+    "template": "CN={{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}"
+  },
   "options": {
     "data_source": "subject.krbprincipalname.0.partition('/')[2].partition('@')[0]"
   }
diff --git a/ipaclient/csrgen/rules/dataSubjectBase.json b/ipaclient/csrgen/rules/dataSubjectBase.json
index 309dfb1..31a38b4 100644
--- a/ipaclient/csrgen/rules/dataSubjectBase.json
+++ b/ipaclient/csrgen/rules/dataSubjectBase.json
@@ -1,14 +1,7 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "{{config.ipacertificatesubjectbase.0}}"
-    },
-    {
-      "helper": "certutil",
-      "template": "{{config.ipacertificatesubjectbase.0|quote}}"
-    }
-  ],
+  "rule": {
+    "template": "{{config.ipacertificatesubjectbase.0}}"
+  },
   "options": {
     "data_source": "config.ipacertificatesubjectbase.0"
   }
diff --git a/ipaclient/csrgen/rules/dataUsernameCN.json b/ipaclient/csrgen/rules/dataUsernameCN.json
index 37e7e01..acbb524 100644
--- a/ipaclient/csrgen/rules/dataUsernameCN.json
+++ b/ipaclient/csrgen/rules/dataUsernameCN.json
@@ -1,14 +1,7 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "CN={{subject.uid.0}}"
-    },
-    {
-      "helper": "certutil",
-      "template": "CN={{subject.uid.0|quote}}"
-    }
-  ],
+  "rule": {
+    "template": "CN={{subject.uid.0}}"
+  },
   "options": {
     "data_source": "subject.uid.0"
   }
diff --git a/ipaclient/csrgen/rules/syntaxSAN.json b/ipaclient/csrgen/rules/syntaxSAN.json
index 122eb12..c6943ed 100644
--- a/ipaclient/csrgen/rules/syntaxSAN.json
+++ b/ipaclient/csrgen/rules/syntaxSAN.json
@@ -1,15 +1,8 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "subjectAltName = @{% call openssl.section() %}{{ datarules|join('\n') }}{% endcall %}",
-      "options": {
-        "extension": true
-      }
-    },
-    {
-      "helper": "certutil",
-      "template": "--extSAN {{ datarules|join(',') }}"
-    }
-  ]
+  "rule": {
+    "template": "subjectAltName = @{% call openssl.section() %}{{ datarules|join('\n') }}{% endcall %}"
+  },
+  "options": {
+    "extension": true
+  }
 }
diff --git a/ipaclient/csrgen/rules/syntaxSubject.json b/ipaclient/csrgen/rules/syntaxSubject.json
index af6ec03..c609e01 100644
--- a/ipaclient/csrgen/rules/syntaxSubject.json
+++ b/ipaclient/csrgen/rules/syntaxSubject.json
@@ -1,14 +1,7 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "distinguished_name = {% call openssl.section() %}{{ datarules|reverse|join('\n') }}{% endcall %}"
-    },
-    {
-      "helper": "certutil",
-      "template": "-s {{ datarules|join(',') }}"
-    }
-  ],
+  "rule": {
+    "template": "distinguished_name = {% call openssl.section() %}{{ datarules|reverse|join('\n') }}{% endcall %}"
+  },
   "options": {
     "required": true,
     "data_source_combinator": "and"
diff --git a/ipaclient/csrgen/templates/certutil_base.tmpl b/ipaclient/csrgen/templates/certutil_base.tmpl
deleted file mode 100644
index a5556fd..0000000
--- a/ipaclient/csrgen/templates/certutil_base.tmpl
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/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/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py
index a0d99ef..c10ef2d 100644
--- a/ipaclient/plugins/csrgen.py
+++ b/ipaclient/plugins/csrgen.py
@@ -106,7 +106,7 @@ def execute(self, *args, **options):
         generator = CSRGenerator(FileRuleProvider())
 
         script = generator.csr_script(
-            principal_obj, config, profile_id, helper)
+            principal_obj, config, profile_id)
 
         result = {}
         if 'out' in options:
diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
new file mode 100644
index 0000000..811bfd7
--- /dev/null
+++ b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
@@ -0,0 +1,34 @@
+#!/bin/bash -e
+
+if [[ $# -lt 2 ]]; then
+echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>"
+echo "Called as: $0 $@"
+exit 1
+fi
+
+CONFIG="$(mktemp)"
+CSR="$1"
+KEYFILE="$2"
+shift; 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 "$KEYFILE" "$@"
+rm "$CONFIG"
diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
new file mode 100644
index 0000000..2edf067
--- /dev/null
+++ b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
@@ -0,0 +1,34 @@
+#!/bin/bash -e
+
+if [[ $# -lt 2 ]]; then
+echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>"
+echo "Called as: $0 $@"
+exit 1
+fi
+
+CONFIG="$(mktemp)"
+CSR="$1"
+KEYFILE="$2"
+shift; shift
+
+echo \
+'[ req ]
+prompt = no
+encrypt_key = no
+
+distinguished_name = sec0
+req_extensions = sec2
+
+[ sec0 ]
+O=DOMAIN.EXAMPLE.COM
+CN=testuser
+
+[ sec1 ]
+email = testu...@example.com
+
+[ sec2 ]
+subjectAltName = @sec1
+' > "$CONFIG"
+
+openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@"
+rm "$CONFIG"
diff --git a/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json b/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json
index feba3e9..094ef71 100644
--- a/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json
+++ b/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json
@@ -1,12 +1,5 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "openssl_rule"
-    },
-    {
-      "helper": "certutil",
-      "template": "certutil_rule"
-    }
-  ]
+  "rule": {
+    "template": "openssl_rule"
+  }
 }
diff --git a/ipatests/test_ipaclient/data/test_csrgen/rules/options.json b/ipatests/test_ipaclient/data/test_csrgen/rules/options.json
index 111a6d8..393ed8c 100644
--- a/ipatests/test_ipaclient/data/test_csrgen/rules/options.json
+++ b/ipatests/test_ipaclient/data/test_csrgen/rules/options.json
@@ -1,18 +1,8 @@
 {
-  "rules": [
-    {
-      "helper": "openssl",
-      "template": "openssl_rule",
-      "options": {
-        "helper_option": true
-      }
-    },
-    {
-      "helper": "certutil",
-      "template": "certutil_rule"
-    }
-  ],
+  "rule": {
+    "template": "openssl_rule"
+  },
   "options": {
-    "global_option": true
+    "rule_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
deleted file mode 100644
index 74a704c..0000000
--- a/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/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" -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
deleted file mode 100644
index 811bfd7..0000000
--- a/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash -e
-
-if [[ $# -lt 2 ]]; then
-echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>"
-echo "Called as: $0 $@"
-exit 1
-fi
-
-CONFIG="$(mktemp)"
-CSR="$1"
-KEYFILE="$2"
-shift; 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 "$KEYFILE" "$@"
-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
deleted file mode 100644
index 4aaeda0..0000000
--- a/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/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" -s CN=testuser,O=DOMAIN.EXAMPLE.COM --extSAN email:testu...@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
deleted file mode 100644
index 2edf067..0000000
--- a/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash -e
-
-if [[ $# -lt 2 ]]; then
-echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>"
-echo "Called as: $0 $@"
-exit 1
-fi
-
-CONFIG="$(mktemp)"
-CSR="$1"
-KEYFILE="$2"
-shift; shift
-
-echo \
-'[ req ]
-prompt = no
-encrypt_key = no
-
-distinguished_name = sec0
-req_extensions = sec2
-
-[ sec0 ]
-O=DOMAIN.EXAMPLE.COM
-CN=testuser
-
-[ sec1 ]
-email = testu...@example.com
-
-[ sec2 ]
-subjectAltName = @sec1
-' > "$CONFIG"
-
-openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@"
-rm "$CONFIG"
diff --git a/ipatests/test_ipaclient/test_csrgen.py b/ipatests/test_ipaclient/test_csrgen.py
index 556f8e0..ae127c9 100644
--- a/ipatests/test_ipaclient/test_csrgen.py
+++ b/ipatests/test_ipaclient/test_csrgen.py
@@ -36,7 +36,7 @@ def __init__(self):
             'example', self.syntax_rule, [self.data_rule])
         self.rules = [self.field_mapping]
 
-    def rules_for_profile(self, profile_id, helper):
+    def rules_for_profile(self, profile_id):
         return self.rules
 
 
@@ -50,10 +50,6 @@ 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}}',
@@ -139,40 +135,23 @@ 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')
+        rule = rule_provider._rule(rule_name)
 
-        assert rule1.template == 'openssl_rule'
-        assert rule2.template == 'certutil_rule'
+        assert rule.template == 'openssl_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')
+        rule = rule_provider._rule(rule_name)
 
-        assert rule1.options['helper_option'] is True
-        assert 'helper_option' not in rule2.options
+        assert rule.options['rule_option'] is True
 
     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')
+            rule_provider._rule('nosuchrule')
 
     def test_rules_for_profile_success(self, rule_provider):
-        rules = rule_provider.rules_for_profile('profile', 'certutil')
+        rules = rule_provider.rules_for_profile('profile')
 
         assert len(rules) == 1
         field_mapping = rules[0]
@@ -182,7 +161,7 @@ def test_rules_for_profile_success(self, rule_provider):
 
     def test_rules_for_profile_nosuchprofile(self, rule_provider):
         with pytest.raises(errors.NotFound):
-            rule_provider.rules_for_profile('nosuchprofile', 'certutil')
+            rule_provider.rules_for_profile('nosuchprofile')
 
 
 class test_CSRGenerator(object):
@@ -197,28 +176,9 @@ def test_userCert_OpenSSL(self, generator):
             ],
         }
 
-        script = generator.csr_script(principal, config, 'userCert', 'openssl')
+        script = generator.csr_script(principal, config, 'userCert')
         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': ['testu...@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:
+                CSR_DATA_DIR, 'configs', 'userCert.conf')) as f:
             expected_script = f.read()
         assert script == expected_script
 
@@ -235,28 +195,9 @@ def test_caIPAserviceCert_OpenSSL(self, generator):
         }
 
         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....@domain.example.com'
-            ],
-        }
-        config = {
-            'ipacertificatesubjectbase': [
-                'O=DOMAIN.EXAMPLE.COM'
-            ],
-        }
-
-        script = generator.csr_script(
-            principal, config, 'caIPAserviceCert', 'certutil')
+            principal, config, 'caIPAserviceCert')
         with open(os.path.join(
-                CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_certutil.sh')) as f:
+                CSR_DATA_DIR, 'configs', 'caIPAserviceCert.conf')) as f:
             expected_script = f.read()
         assert script == expected_script
 
@@ -267,10 +208,11 @@ def test_optionalAttributeMissing(self, generator):
         rule_provider = StubRuleProvider()
         rule_provider.data_rule.template = '{{subject.mail}}'
         rule_provider.data_rule.options = {'data_source': 'subject.mail'}
-        generator = IdentityCSRGenerator(rule_provider)
+        generator = csrgen.CSRGenerator(
+            rule_provider, formatter_class=IdentityFormatter)
 
         script = generator.csr_script(
-            principal, {}, 'example', 'identity')
+            principal, {}, 'example')
         assert script == '\n'
 
     def test_twoDataRulesOneMissing(self, generator):
@@ -280,9 +222,10 @@ def test_twoDataRulesOneMissing(self, generator):
         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)
+        generator = csrgen.CSRGenerator(
+            rule_provider, formatter_class=IdentityFormatter)
 
-        script = generator.csr_script(principal, {}, 'example', 'identity')
+        script = generator.csr_script(principal, {}, 'example')
         assert script == ',testuser\n'
 
     def test_requiredAttributeMissing(self):
@@ -291,8 +234,9 @@ def test_requiredAttributeMissing(self):
         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)
+        generator = csrgen.CSRGenerator(
+            rule_provider, formatter_class=IdentityFormatter)
 
         with pytest.raises(errors.CSRTemplateError):
             _script = generator.csr_script(
-                principal, {}, 'example', 'identity')
+                principal, {}, 'example')

From 136c6c3e2a4f77a27f435efd4a1cd95c9e089314 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Tue, 21 Mar 2017 17:23:46 -0400
Subject: [PATCH 2/8] csrgen: Change to pure openssl config format (no script)

https://pagure.io/freeipa/issue/4899

Reviewed-By: Jan Cholasta <jchol...@redhat.com>
---
 ipaclient/csrgen.py                                | 10 +++++-----
 ipaclient/csrgen/templates/openssl_base.tmpl       | 22 ++--------------------
 ipaclient/plugins/csrgen.py                        |  3 +--
 .../data/test_csrgen/configs/caIPAserviceCert.conf | 20 +-------------------
 .../data/test_csrgen/configs/userCert.conf         | 20 +-------------------
 ipatests/test_ipaclient/test_csrgen.py             | 10 +++++-----
 6 files changed, 15 insertions(+), 70 deletions(-)

diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py
index 8ca0722..eca99a1 100644
--- a/ipaclient/csrgen.py
+++ b/ipaclient/csrgen.py
@@ -66,7 +66,7 @@ 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.
+    config, which specifies how to build a CSR.
 
     Subclasses of Formatter should set the value of base_template_name to the
     filename of a base template with spaces for the processed rules.
@@ -214,7 +214,7 @@ def _get_template_params(self, syntax_rules):
 
 
 class OpenSSLFormatter(Formatter):
-    """Formatter class supporting the openssl command-line tool."""
+    """Formatter class generating the openssl config-file format."""
 
     base_template_name = 'openssl_base.tmpl'
 
@@ -359,17 +359,17 @@ def __init__(self, rule_provider, formatter_class=OpenSSLFormatter):
         self.rule_provider = rule_provider
         self.formatter = formatter_class()
 
-    def csr_script(self, principal, config, profile_id):
+    def csr_config(self, principal, config, profile_id):
         render_data = {'subject': principal, 'config': config}
 
         rules = self.rule_provider.rules_for_profile(profile_id)
         template = self.formatter.build_template(rules)
 
         try:
-            script = template.render(render_data)
+            config = template.render(render_data)
         except jinja2.UndefinedError:
             logger.debug(traceback.format_exc())
             raise errors.CSRTemplateError(reason=_(
                 'Template error when formatting certificate data'))
 
-        return script
+        return config
diff --git a/ipaclient/csrgen/templates/openssl_base.tmpl b/ipaclient/csrgen/templates/openssl_base.tmpl
index 22b1686..8d37994 100644
--- a/ipaclient/csrgen/templates/openssl_base.tmpl
+++ b/ipaclient/csrgen/templates/openssl_base.tmpl
@@ -1,21 +1,6 @@
 {% raw -%}
 {% import "openssl_macros.tmpl" as openssl -%}
-{%- endraw %}
-#!/bin/bash -e
-
-if [[ $# -lt 2 ]]; then
-echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>"
-echo "Called as: $0 $@"
-exit 1
-fi
-
-CONFIG="$(mktemp)"
-CSR="$1"
-KEYFILE="$2"
-shift; shift
-
-echo \
-{% raw %}{% filter quote %}{% endraw -%}
+{% endraw -%}
 [ req ]
 prompt = no
 encrypt_key = no
@@ -29,7 +14,4 @@ encrypt_key = no
 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 "$KEYFILE" "$@"
-rm "$CONFIG"
+{%- endraw %}
diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py
index c10ef2d..15ed791 100644
--- a/ipaclient/plugins/csrgen.py
+++ b/ipaclient/plugins/csrgen.py
@@ -105,8 +105,7 @@ def execute(self, *args, **options):
 
         generator = CSRGenerator(FileRuleProvider())
 
-        script = generator.csr_script(
-            principal_obj, config, profile_id)
+        script = generator.csr_config(principal_obj, config, profile_id)
 
         result = {}
         if 'out' in options:
diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
index 811bfd7..3724bdc 100644
--- a/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
+++ b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf
@@ -1,18 +1,4 @@
-#!/bin/bash -e
-
-if [[ $# -lt 2 ]]; then
-echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>"
-echo "Called as: $0 $@"
-exit 1
-fi
-
-CONFIG="$(mktemp)"
-CSR="$1"
-KEYFILE="$2"
-shift; shift
-
-echo \
-'[ req ]
+[ req ]
 prompt = no
 encrypt_key = no
 
@@ -28,7 +14,3 @@ DNS = machine.example.com
 
 [ sec2 ]
 subjectAltName = @sec1
-' > "$CONFIG"
-
-openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@"
-rm "$CONFIG"
diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
index 2edf067..00d63de 100644
--- a/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
+++ b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf
@@ -1,18 +1,4 @@
-#!/bin/bash -e
-
-if [[ $# -lt 2 ]]; then
-echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>"
-echo "Called as: $0 $@"
-exit 1
-fi
-
-CONFIG="$(mktemp)"
-CSR="$1"
-KEYFILE="$2"
-shift; shift
-
-echo \
-'[ req ]
+[ req ]
 prompt = no
 encrypt_key = no
 
@@ -28,7 +14,3 @@ email = testu...@example.com
 
 [ sec2 ]
 subjectAltName = @sec1
-' > "$CONFIG"
-
-openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@"
-rm "$CONFIG"
diff --git a/ipatests/test_ipaclient/test_csrgen.py b/ipatests/test_ipaclient/test_csrgen.py
index ae127c9..d0798f8 100644
--- a/ipatests/test_ipaclient/test_csrgen.py
+++ b/ipatests/test_ipaclient/test_csrgen.py
@@ -176,7 +176,7 @@ def test_userCert_OpenSSL(self, generator):
             ],
         }
 
-        script = generator.csr_script(principal, config, 'userCert')
+        script = generator.csr_config(principal, config, 'userCert')
         with open(os.path.join(
                 CSR_DATA_DIR, 'configs', 'userCert.conf')) as f:
             expected_script = f.read()
@@ -194,7 +194,7 @@ def test_caIPAserviceCert_OpenSSL(self, generator):
             ],
         }
 
-        script = generator.csr_script(
+        script = generator.csr_config(
             principal, config, 'caIPAserviceCert')
         with open(os.path.join(
                 CSR_DATA_DIR, 'configs', 'caIPAserviceCert.conf')) as f:
@@ -211,7 +211,7 @@ def test_optionalAttributeMissing(self, generator):
         generator = csrgen.CSRGenerator(
             rule_provider, formatter_class=IdentityFormatter)
 
-        script = generator.csr_script(
+        script = generator.csr_config(
             principal, {}, 'example')
         assert script == '\n'
 
@@ -225,7 +225,7 @@ def test_twoDataRulesOneMissing(self, generator):
         generator = csrgen.CSRGenerator(
             rule_provider, formatter_class=IdentityFormatter)
 
-        script = generator.csr_script(principal, {}, 'example')
+        script = generator.csr_config(principal, {}, 'example')
         assert script == ',testuser\n'
 
     def test_requiredAttributeMissing(self):
@@ -238,5 +238,5 @@ def test_requiredAttributeMissing(self):
             rule_provider, formatter_class=IdentityFormatter)
 
         with pytest.raises(errors.CSRTemplateError):
-            _script = generator.csr_script(
+            _script = generator.csr_config(
                 principal, {}, 'example')

From e7588ab2dc73e7f66ebc6cdcfb99470540e37731 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Fri, 6 Jan 2017 11:19:19 -0500
Subject: [PATCH 3/8] csrgen: Modify cert_get_requestdata to return a
 CertificationRequestInfo

Also modify cert_request to use this new format. Note, only PEM private
keys are supported for now. NSS databases are not.

https://pagure.io/freeipa/issue/4899

Reviewed-By: Jan Cholasta <jchol...@redhat.com>
---
 ipaclient/csrgen.py         |  75 +++++++++++-
 ipaclient/csrgen_ffi.py     | 291 ++++++++++++++++++++++++++++++++++++++++++++
 ipaclient/plugins/cert.py   |  75 +++++-------
 ipaclient/plugins/csrgen.py |  35 +++---
 4 files changed, 415 insertions(+), 61 deletions(-)
 create mode 100644 ipaclient/csrgen_ffi.py

diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py
index eca99a1..04d2821 100644
--- a/ipaclient/csrgen.py
+++ b/ipaclient/csrgen.py
@@ -7,13 +7,22 @@
 import json
 import os.path
 import pipes
+import subprocess
 import traceback
 
 import pkg_resources
 
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.serialization import (
+    load_pem_private_key, Encoding, PublicFormat)
 import jinja2
 import jinja2.ext
 import jinja2.sandbox
+from pyasn1.codec.der import decoder, encoder
+from pyasn1.type import univ
+from pyasn1_modules import rfc2314
 import six
 
 from ipalib import api
@@ -56,7 +65,8 @@ def quote(self, data):
     def required(self, data, name):
         if not data:
             raise errors.CSRTemplateError(
-                reason=_('Required CSR generation rule %(name)s is missing data') %
+                reason=_(
+                    'Required CSR generation rule %(name)s is missing data') %
                 {'name': name})
         return data
 
@@ -373,3 +383,66 @@ def csr_config(self, principal, config, profile_id):
                 'Template error when formatting certificate data'))
 
         return config
+
+
+class CSRLibraryAdaptor(object):
+    def get_subject_public_key_info(self):
+        raise NotImplementedError('Use a subclass of CSRLibraryAdaptor')
+
+    def sign_csr(self, certification_request_info):
+        """Sign a CertificationRequestInfo.
+
+        Returns: str, a DER-encoded signed CSR.
+        """
+        raise NotImplementedError('Use a subclass of CSRLibraryAdaptor')
+
+
+class OpenSSLAdaptor(object):
+    def __init__(self, key_filename, password_filename):
+        self.key_filename = key_filename
+        self.password_filename = password_filename
+
+    def key(self):
+        with open(self.key_filename, 'r') as key_file:
+            key_bytes = key_file.read()
+        password = None
+        if self.password_filename is not None:
+            with open(self.password_filename, 'r') as password_file:
+                password = password_file.read().strip()
+
+        key = load_pem_private_key(key_bytes, password, default_backend())
+        return key
+
+    def get_subject_public_key_info(self):
+        pubkey_info = self.key().public_key().public_bytes(
+            Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
+        return pubkey_info
+
+    def sign_csr(self, certification_request_info):
+        reqinfo = decoder.decode(
+            certification_request_info, rfc2314.CertificationRequestInfo())[0]
+        csr = rfc2314.CertificationRequest()
+        csr.setComponentByName('certificationRequestInfo', reqinfo)
+
+        algorithm = rfc2314.SignatureAlgorithmIdentifier()
+        algorithm.setComponentByName(
+            'algorithm', univ.ObjectIdentifier(
+                '1.2.840.113549.1.1.11'))  # sha256WithRSAEncryption
+        csr.setComponentByName('signatureAlgorithm', algorithm)
+
+        signature = self.key().sign(
+            certification_request_info,
+            padding.PKCS1v15(),
+            hashes.SHA256()
+        )
+        asn1sig = univ.BitString("'%s'H" % signature.encode('hex'))
+        csr.setComponentByName('signature', asn1sig)
+        return encoder.encode(csr)
+
+
+class NSSAdaptor(object):
+    def get_subject_public_key_info(self):
+        raise NotImplementedError('NSS is not yet supported')
+
+    def sign_csr(self, certification_request_info):
+        raise NotImplementedError('NSS is not yet supported')
diff --git a/ipaclient/csrgen_ffi.py b/ipaclient/csrgen_ffi.py
new file mode 100644
index 0000000..c45db5f
--- /dev/null
+++ b/ipaclient/csrgen_ffi.py
@@ -0,0 +1,291 @@
+#!/usr/bin/python
+
+from cffi import FFI
+import ctypes.util
+
+from ipalib import errors
+
+_ffi = FFI()
+
+_ffi.cdef('''
+typedef ... CONF;
+typedef ... CONF_METHOD;
+typedef ... BIO;
+typedef ... ipa_STACK_OF_CONF_VALUE;
+
+/* openssl/conf.h */
+typedef struct {
+    char *section;
+    char *name;
+    char *value;
+} CONF_VALUE;
+
+CONF *NCONF_new(CONF_METHOD *meth);
+void NCONF_free(CONF *conf);
+int NCONF_load_bio(CONF *conf, BIO *bp, long *eline);
+ipa_STACK_OF_CONF_VALUE *NCONF_get_section(const CONF *conf,
+                                        const char *section);
+char *NCONF_get_string(const CONF *conf, const char *group, const char *name);
+
+/* openssl/safestack.h */
+// int sk_CONF_VALUE_num(ipa_STACK_OF_CONF_VALUE *);
+// CONF_VALUE *sk_CONF_VALUE_value(ipa_STACK_OF_CONF_VALUE *, int);
+
+/* openssl/stack.h */
+typedef ... _STACK;
+
+int sk_num(const _STACK *);
+void *sk_value(const _STACK *, int);
+
+/* openssl/bio.h */
+BIO *BIO_new_mem_buf(const void *buf, int len);
+int BIO_free(BIO *a);
+
+/* openssl/asn1.h */
+typedef struct ASN1_ENCODING_st {
+    unsigned char *enc;         /* DER encoding */
+    long len;                   /* Length of encoding */
+    int modified;               /* set to 1 if 'enc' is invalid */
+} ASN1_ENCODING;
+
+/* openssl/evp.h */
+typedef ... EVP_PKEY;
+
+void EVP_PKEY_free(EVP_PKEY *pkey);
+
+/* openssl/x509.h */
+typedef ... ASN1_INTEGER;
+typedef ... ASN1_BIT_STRING;
+typedef ... X509;
+typedef ... X509_ALGOR;
+typedef ... X509_CRL;
+typedef ... X509_NAME;
+typedef ... X509_PUBKEY;
+typedef ... ipa_STACK_OF_X509_ATTRIBUTE;
+
+typedef struct X509_req_info_st {
+    ASN1_ENCODING enc;
+    ASN1_INTEGER *version;
+    X509_NAME *subject;
+    X509_PUBKEY *pubkey;
+    /*  d=2 hl=2 l=  0 cons: cont: 00 */
+    ipa_STACK_OF_X509_ATTRIBUTE *attributes; /* [ 0 ] */
+} X509_REQ_INFO;
+
+typedef struct X509_req_st {
+    X509_REQ_INFO *req_info;
+    X509_ALGOR *sig_alg;
+    ASN1_BIT_STRING *signature;
+    int references;
+} X509_REQ;
+
+X509_REQ *X509_REQ_new(void);
+void X509_REQ_free(X509_REQ *);
+EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a);
+int X509_REQ_set_pubkey(X509_REQ *x, EVP_PKEY *pkey);
+int X509_NAME_add_entry_by_txt(X509_NAME *name, const char *field, int type,
+                               const unsigned char *bytes, int len, int loc,
+                               int set);
+int X509_NAME_entry_count(X509_NAME *name);
+int i2d_X509_REQ_INFO(X509_REQ_INFO *a, unsigned char **out); \
+
+/* openssl/x509v3.h */
+typedef ... X509V3_CONF_METHOD;
+
+typedef struct v3_ext_ctx {
+    int flags;
+    X509 *issuer_cert;
+    X509 *subject_cert;
+    X509_REQ *subject_req;
+    X509_CRL *crl;
+    X509V3_CONF_METHOD *db_meth;
+    void *db;
+} X509V3_CTX;
+
+void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject,
+                    X509_REQ *req, X509_CRL *crl, int flags);
+void X509V3_set_nconf(X509V3_CTX *ctx, CONF *conf);
+int X509V3_EXT_REQ_add_nconf(CONF *conf, X509V3_CTX *ctx, char *section,
+                             X509_REQ *req);
+
+/* openssl/x509v3.h */
+unsigned long ERR_get_error(void);
+char *ERR_error_string(unsigned long e, char *buf);
+''')
+
+_libcrypto = _ffi.dlopen(ctypes.util.find_library('crypto'))
+
+NULL = _ffi.NULL
+
+# openssl/conf.h
+NCONF_new = _libcrypto.NCONF_new
+NCONF_free = _libcrypto.NCONF_free
+NCONF_load_bio = _libcrypto.NCONF_load_bio
+NCONF_get_section = _libcrypto.NCONF_get_section
+NCONF_get_string = _libcrypto.NCONF_get_string
+
+# openssl/stack.h
+sk_num = _libcrypto.sk_num
+sk_value = _libcrypto.sk_value
+
+
+def sk_CONF_VALUE_num(sk):
+    return sk_num(_ffi.cast("_STACK *", sk))
+
+
+def sk_CONF_VALUE_value(sk, i):
+    return _ffi.cast("CONF_VALUE *", sk_value(_ffi.cast("_STACK *", sk), i))
+
+
+# openssl/bio.h
+BIO_new_mem_buf = _libcrypto.BIO_new_mem_buf
+BIO_free = _libcrypto.BIO_free
+
+# openssl/x509.h
+X509_REQ_new = _libcrypto.X509_REQ_new
+X509_REQ_free = _libcrypto.X509_REQ_free
+X509_REQ_set_pubkey = _libcrypto.X509_REQ_set_pubkey
+d2i_PUBKEY_bio = _libcrypto.d2i_PUBKEY_bio
+i2d_X509_REQ_INFO = _libcrypto.i2d_X509_REQ_INFO
+X509_NAME_add_entry_by_txt = _libcrypto.X509_NAME_add_entry_by_txt
+X509_NAME_entry_count = _libcrypto.X509_NAME_entry_count
+
+
+def X509_REQ_get_subject_name(req):
+    return req.req_info.subject
+
+# openssl/evp.h
+EVP_PKEY_free = _libcrypto.EVP_PKEY_free
+
+# openssl/asn1.h
+MBSTRING_UTF8 = 0x1000
+
+# openssl/x509v3.h
+X509V3_set_ctx = _libcrypto.X509V3_set_ctx
+X509V3_set_nconf = _libcrypto.X509V3_set_nconf
+X509V3_EXT_REQ_add_nconf = _libcrypto.X509V3_EXT_REQ_add_nconf
+
+# openssl/err.h
+ERR_get_error = _libcrypto.ERR_get_error
+ERR_error_string = _libcrypto.ERR_error_string
+
+
+def _raise_openssl_errors():
+    msgs = []
+
+    code = ERR_get_error()
+    while code != 0:
+        msg = ERR_error_string(code, NULL)
+        msgs.append(_ffi.string(msg))
+        code = ERR_get_error()
+
+    raise errors.CSRTemplateError(reason='\n'.join(msgs))
+
+
+def _parse_dn_section(subj, dn_sk):
+    for i in range(sk_CONF_VALUE_num(dn_sk)):
+        v = sk_CONF_VALUE_value(dn_sk, i)
+        rdn_type = _ffi.string(v.name)
+
+        # Skip past any leading X. X: X, etc to allow for multiple instances
+        for idx, c in enumerate(rdn_type):
+            if c in ':,.':
+                if idx+1 < len(rdn_type):
+                    rdn_type = rdn_type[idx+1:]
+                break
+        if rdn_type.startswith('+'):
+            rdn_type = rdn_type[1:]
+            mval = -1
+        else:
+            mval = 0
+        if not X509_NAME_add_entry_by_txt(
+                subj, rdn_type, MBSTRING_UTF8, v.value, -1, -1, mval):
+            _raise_openssl_errors()
+
+    if not X509_NAME_entry_count(subj):
+        raise errors.CSRTemplateError(
+            reason='error, subject in config file is empty')
+
+
+def build_requestinfo(config, public_key_info):
+    reqdata = NULL
+    req = NULL
+    nconf_bio = NULL
+    pubkey_bio = NULL
+    pubkey = NULL
+
+    try:
+        reqdata = NCONF_new(NULL)
+        if reqdata == NULL:
+            _raise_openssl_errors()
+
+        nconf_bio = BIO_new_mem_buf(config, len(config))
+        errorline = _ffi.new('long[1]', [-1])
+        i = NCONF_load_bio(reqdata, nconf_bio, errorline)
+        if i < 0:
+            if errorline[0] < 0:
+                raise errors.CSRTemplateError(reason="Can't load config file")
+            else:
+                raise errors.CSRTemplateError(
+                    reason='Error on line %d of config file' % errorline[0])
+
+        dn_sect = NCONF_get_string(reqdata, 'req', 'distinguished_name')
+        if dn_sect == NULL:
+            raise errors.CSRTemplateError(
+                reason='Unable to find "distinguished_name" key in config')
+
+        dn_sk = NCONF_get_section(reqdata, dn_sect)
+        if dn_sk == NULL:
+            raise errors.CSRTemplateError(
+                reason='Unable to find "%s" section in config' %
+                _ffi.string(dn_sect))
+
+        pubkey_bio = BIO_new_mem_buf(public_key_info, len(public_key_info))
+        pubkey = d2i_PUBKEY_bio(pubkey_bio, NULL)
+        if pubkey == NULL:
+            _raise_openssl_errors()
+
+        req = X509_REQ_new()
+        if req == NULL:
+            _raise_openssl_errors()
+
+        subject = X509_REQ_get_subject_name(req)
+
+        _parse_dn_section(subject, dn_sk)
+
+        if not X509_REQ_set_pubkey(req, pubkey):
+            _raise_openssl_errors()
+
+        ext_ctx = _ffi.new("X509V3_CTX[1]")
+        X509V3_set_ctx(ext_ctx, NULL, NULL, req, NULL, 0)
+        X509V3_set_nconf(ext_ctx, reqdata)
+
+        extn_section = NCONF_get_string(reqdata, "req", "req_extensions")
+        if extn_section != NULL:
+            if not X509V3_EXT_REQ_add_nconf(
+                    reqdata, ext_ctx, extn_section, req):
+                _raise_openssl_errors()
+
+        der_len = i2d_X509_REQ_INFO(req.req_info, NULL)
+        if der_len < 0:
+            _raise_openssl_errors()
+
+        der_buf = _ffi.new("unsigned char[%d]" % der_len)
+        der_out = _ffi.new("unsigned char **", der_buf)
+        der_len = i2d_X509_REQ_INFO(req.req_info, der_out)
+        if der_len < 0:
+            _raise_openssl_errors()
+
+        return _ffi.buffer(der_buf, der_len)
+
+    finally:
+        if reqdata != NULL:
+            NCONF_free(reqdata)
+        if req != NULL:
+            X509_REQ_free(req)
+        if nconf_bio != NULL:
+            BIO_free(nconf_bio)
+        if pubkey_bio != NULL:
+            BIO_free(pubkey_bio)
+        if pubkey != NULL:
+            EVP_PKEY_free(pubkey)
diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 9ec6970..a4ee9a9 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -20,11 +20,10 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import base64
-import subprocess
-from tempfile import NamedTemporaryFile as NTF
 
 import six
 
+from ipaclient import csrgen
 from ipaclient.frontend import MethodOverride
 from ipalib import errors
 from ipalib import x509
@@ -108,54 +107,40 @@ def forward(self, csr=None, **options):
 
         if csr is None:
             if database:
-                helper = u'certutil'
-                helper_args = ['-d', database]
-                if password_file:
-                    helper_args += ['-f', password_file]
+                adaptor = csrgen.NSSAdaptor(database, password_file)
             elif private_key:
-                helper = u'openssl'
-                helper_args = [private_key]
-                if password_file:
-                    helper_args += ['-passin', 'file:%s' % password_file]
+                adaptor = csrgen.OpenSSLAdaptor(private_key, password_file)
             else:
                 raise errors.InvocationError(
                     message=u"One of 'database' or 'private_key' is required")
 
-            with NTF() as scriptfile, NTF() as csrfile:
-                # If csr_profile_id is passed, that takes precedence.
-                # Otherwise, use profile_id. If neither are passed, the default
-                # in cert_get_requestdata will be used.
-                profile_id = csr_profile_id
-                if profile_id is None:
-                    profile_id = options.get('profile_id')
-
-                self.api.Command.cert_get_requestdata(
-                    profile_id=profile_id,
-                    principal=options.get('principal'),
-                    out=unicode(scriptfile.name),
-                    helper=helper)
-
-                helper_cmd = [
-                    'bash', '-e', scriptfile.name, csrfile.name] + helper_args
-
-                try:
-                    subprocess.check_output(helper_cmd)
-                except subprocess.CalledProcessError as e:
-                    raise errors.CertificateOperationError(
-                        error=(
-                            _('Error running "%(cmd)s" to generate CSR:'
-                              ' %(err)s') %
-                            {'cmd': ' '.join(helper_cmd), 'err': e.output}))
-
-                try:
-                    csr = unicode(csrfile.read())
-                except IOError as e:
-                    raise errors.CertificateOperationError(
-                        error=(_('Unable to read generated CSR file: %(err)s')
-                               % {'err': e}))
-                if not csr:
-                    raise errors.CertificateOperationError(
-                        error=(_('Generated CSR was empty')))
+            pubkey_info = adaptor.get_subject_public_key_info()
+            pubkey_info_b64 = base64.b64encode(pubkey_info)
+
+            # If csr_profile_id is passed, that takes precedence.
+            # Otherwise, use profile_id. If neither are passed, the default
+            # in cert_get_requestdata will be used.
+            profile_id = csr_profile_id
+            if profile_id is None:
+                profile_id = options.get('profile_id')
+
+            response = self.api.Command.cert_get_requestdata(
+                profile_id=profile_id,
+                principal=options.get('principal'),
+                public_key_info=unicode(pubkey_info_b64))
+
+            req_info_b64 = response['result']['request_info']
+            req_info = base64.b64decode(req_info_b64)
+
+            csr = adaptor.sign_csr(req_info)
+
+            if not csr:
+                raise errors.CertificateOperationError(
+                    error=(_('Generated CSR was empty')))
+
+            # cert_request requires the CSR to be base64-encoded (but PEM
+            # header and footer are not required)
+            csr = unicode(base64.b64encode(csr))
         else:
             if database is not None or private_key is not None:
                 raise errors.MutuallyExclusiveError(reason=_(
diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py
index 15ed791..568a79f 100644
--- a/ipaclient/plugins/csrgen.py
+++ b/ipaclient/plugins/csrgen.py
@@ -2,15 +2,18 @@
 # Copyright (C) 2016  FreeIPA Contributors see COPYING for license
 #
 
+import base64
+
 import six
 
-from ipaclient.csrgen import CSRGenerator, FileRuleProvider
+from ipaclient import csrgen
+from ipaclient import csrgen_ffi
 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.parameters import File, Principal
 from ipalib.plugable import Registry
 from ipalib.text import _
 from ipapython import dogtag
@@ -43,15 +46,14 @@ class cert_get_requestdata(Local):
             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'),
+        File(
+            'public_key_info',
+            label=_('Subject Public Key Info'),
+            doc=_('DER-encoded SubjectPublicKeyInfo structure'),
         ),
         Str(
             'out?',
-            doc=_('Write CSR generation script to file'),
+            doc=_('Write CertificationRequestInfo to file'),
         ),
     )
 
@@ -65,8 +67,8 @@ class cert_get_requestdata(Local):
 
     has_output_params = (
         Str(
-            'script',
-            label=_('Generation script'),
+            'request_info',
+            label=_('CertificationRequestInfo structure'),
         )
     )
 
@@ -78,7 +80,8 @@ def execute(self, *args, **options):
         profile_id = options.get('profile_id')
         if profile_id is None:
             profile_id = dogtag.DEFAULT_PROFILE
-        helper = options.get('helper')
+        public_key_info = options.get('public_key_info')
+        public_key_info = base64.b64decode(public_key_info)
 
         if self.api.env.in_server:
             backend = self.api.Backend.ldap2
@@ -103,16 +106,18 @@ def execute(self, *args, **options):
         principal_obj = principal_obj['result']
         config = api.Command.config_show()['result']
 
-        generator = CSRGenerator(FileRuleProvider())
+        generator = csrgen.CSRGenerator(csrgen.FileRuleProvider())
 
-        script = generator.csr_config(principal_obj, config, profile_id)
+        csr_config = generator.csr_config(principal_obj, config, profile_id)
+        request_info = base64.b64encode(csrgen_ffi.build_requestinfo(
+            csr_config.encode('utf8'), public_key_info))
 
         result = {}
         if 'out' in options:
             with open(options['out'], 'wb') as f:
-                f.write(script)
+                f.write(request_info)
         else:
-            result = dict(script=script)
+            result = dict(request_info=request_info)
 
         return dict(
             result=result

From a53e17830c3d4fd59a62248d4447491675c6a80e Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 30 Jan 2017 10:51:11 -0500
Subject: [PATCH 4/8] csrgen: Beginnings of NSS database support

https://pagure.io/freeipa/issue/4899

Reviewed-By: Jan Cholasta <jchol...@redhat.com>
---
 ipaclient/csrgen.py | 27 ++++++++++++++++++++++++++-
 1 file changed, 26 insertions(+), 1 deletion(-)

diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py
index 04d2821..0f52a8b 100644
--- a/ipaclient/csrgen.py
+++ b/ipaclient/csrgen.py
@@ -2,9 +2,11 @@
 # Copyright (C) 2016  FreeIPA Contributors see COPYING for license
 #
 
+import base64
 import collections
 import errno
 import json
+import os
 import os.path
 import pipes
 import subprocess
@@ -17,6 +19,7 @@
 from cryptography.hazmat.primitives import hashes
 from cryptography.hazmat.primitives.serialization import (
     load_pem_private_key, Encoding, PublicFormat)
+from cryptography.x509 import load_pem_x509_certificate
 import jinja2
 import jinja2.ext
 import jinja2.sandbox
@@ -441,8 +444,30 @@ def sign_csr(self, certification_request_info):
 
 
 class NSSAdaptor(object):
+    def __init__(self, database, password_filename):
+        self.database = database
+        self.password_filename = password_filename
+        self.nickname = base64.b32encode(os.urandom(40))
+
     def get_subject_public_key_info(self):
-        raise NotImplementedError('NSS is not yet supported')
+        temp_cn = base64.b32encode(os.urandom(40))
+
+        password_args = []
+        if self.password_filename is not None:
+            password_args = ['-f', self.password_filename]
+
+        subprocess.check_call(
+            ['certutil', '-S', '-n', self.nickname, '-s', 'CN=%s' % temp_cn,
+             '-x', '-t', ',,', '-d', self.database] + password_args)
+        cert_pem = subprocess.check_output(
+            ['certutil', '-L', '-n', self.nickname, '-a',
+             '-d', self.database] + password_args)
+
+        cert = load_pem_x509_certificate(cert_pem, default_backend())
+        pubkey_info = cert.public_key().public_bytes(
+            Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
+
+        return pubkey_info
 
     def sign_csr(self, certification_request_info):
         raise NotImplementedError('NSS is not yet supported')

From 5433ed41af1e73c7fc7ab7b94b9eecf99fa45130 Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Tue, 17 Jan 2017 08:49:54 +0100
Subject: [PATCH 5/8] Conditionally import pyhbac

The pyhbac module is part of SSSD. It's not available as stand-alone
PyPI package. It would take a lot of effort to package it because the
code is deeply tight into SSSD.

Let's follow the example of other SSSD Python packages and make the
import of pyhbac conditionally. It's only necessary for caacl and
hbactest plugins.

I renamed convert_to_ipa_rule() to _convert_to_ipa_rule() because it
does not check for presence of pyhbac package itself. The check is
performed earlier in execute(). The prefix indicates that it is an
internal function and developers have to think twice before using it
in another place.

This makes it much easier to install ipaserver with instrumented build
of Python with a different ABI or in isolated virtual envs to profile
and debug the server.

Signed-off-by: Christian Heimes <chei...@redhat.com>
---
 ipaserver/plugins/caacl.py    | 86 -----------------------------------------
 ipaserver/plugins/cert.py     | 90 ++++++++++++++++++++++++++++++++++++++++++-
 ipaserver/plugins/hbactest.py | 19 +++++++--
 3 files changed, 105 insertions(+), 90 deletions(-)

diff --git a/ipaserver/plugins/caacl.py b/ipaserver/plugins/caacl.py
index ff1178a..43a397d 100644
--- a/ipaserver/plugins/caacl.py
+++ b/ipaserver/plugins/caacl.py
@@ -2,12 +2,10 @@
 # Copyright (C) 2015  FreeIPA Contributors see COPYING for license
 #
 
-import pyhbac
 import six
 
 from ipalib import api, errors, output
 from ipalib import Bool, Str, StrEnum
-from ipalib.constants import IPA_CA_CN
 from ipalib.plugable import Registry
 from .baseldap import (
     LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, LDAPQuery,
@@ -80,90 +78,6 @@
 register = Registry()
 
 
-def _acl_make_request(principal_type, principal, ca_id, profile_id):
-    """Construct HBAC request for the given principal, CA and profile"""
-
-    req = pyhbac.HbacRequest()
-    req.targethost.name = ca_id
-    req.service.name = profile_id
-    if principal_type == 'user':
-        req.user.name = principal.username
-    elif principal_type == 'host':
-        req.user.name = principal.hostname
-    elif principal_type == 'service':
-        req.user.name = unicode(principal)
-    groups = []
-    if principal_type == 'user':
-        user_obj = api.Command.user_show(principal.username)['result']
-        groups = user_obj.get('memberof_group', [])
-        groups += user_obj.get('memberofindirect_group', [])
-    elif principal_type == 'host':
-        host_obj = api.Command.host_show(principal.hostname)['result']
-        groups = host_obj.get('memberof_hostgroup', [])
-        groups += host_obj.get('memberofindirect_hostgroup', [])
-    req.user.groups = sorted(set(groups))
-    return req
-
-
-def _acl_make_rule(principal_type, obj):
-    """Turn CA ACL object into HBAC rule.
-
-    ``principal_type``
-        String in {'user', 'host', 'service'}
-    """
-    rule = pyhbac.HbacRule(obj['cn'][0])
-    rule.enabled = obj['ipaenabledflag'][0]
-    rule.srchosts.category = {pyhbac.HBAC_CATEGORY_ALL}
-
-    # add CA(s)
-    if 'ipacacategory' in obj and obj['ipacacategory'][0].lower() == 'all':
-        rule.targethosts.category = {pyhbac.HBAC_CATEGORY_ALL}
-    else:
-        # For compatibility with pre-lightweight-CAs CA ACLs,
-        # no CA members implies the host authority (only)
-        rule.targethosts.names = obj.get('ipamemberca_ca', [IPA_CA_CN])
-
-    # add profiles
-    if ('ipacertprofilecategory' in obj
-            and obj['ipacertprofilecategory'][0].lower() == 'all'):
-        rule.services.category = {pyhbac.HBAC_CATEGORY_ALL}
-    else:
-        attr = 'ipamembercertprofile_certprofile'
-        rule.services.names = obj.get(attr, [])
-
-    # add principals and principal's groups
-    category_attr = '{}category'.format(principal_type)
-    if category_attr in obj and obj[category_attr][0].lower() == 'all':
-        rule.users.category = {pyhbac.HBAC_CATEGORY_ALL}
-    else:
-        if principal_type == 'user':
-            rule.users.names = obj.get('memberuser_user', [])
-            rule.users.groups = obj.get('memberuser_group', [])
-        elif principal_type == 'host':
-            rule.users.names = obj.get('memberhost_host', [])
-            rule.users.groups = obj.get('memberhost_hostgroup', [])
-        elif principal_type == 'service':
-            rule.users.names = [
-                unicode(principal)
-                for principal in obj.get('memberservice_service', [])
-            ]
-
-    return rule
-
-
-def acl_evaluate(principal, ca_id, profile_id):
-    if principal.is_user:
-        principal_type = 'user'
-    elif principal.is_host:
-        principal_type = 'host'
-    else:
-        principal_type = 'service'
-    req = _acl_make_request(principal_type, principal, ca_id, profile_id)
-    acls = api.Command.caacl_find(no_members=False)['result']
-    rules = [_acl_make_rule(principal_type, obj) for obj in acls]
-    return req.evaluate(rules) == pyhbac.HBAC_EVAL_ALLOW
-
-
 @register()
 class caacl(LDAPObject):
     """
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index dfc7444..5590913 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -43,7 +43,6 @@
 from .virtual import VirtualCommand
 from .baseldap import pkey_to_value
 from .certprofile import validate_profile_id
-from .caacl import acl_evaluate
 from ipalib.text import _
 from ipalib.request import context
 from ipalib import output
@@ -52,6 +51,11 @@
 from ipapython.ipa_log_manager import root_logger
 from ipaserver.plugins.service import normalize_principal, validate_realm
 
+try:
+    import pyhbac
+except ImportError:
+    raise errors.SkipPluginModule(reason=_('pyhbac is not installed.'))
+
 if six.PY3:
     unicode = str
 
@@ -158,6 +162,90 @@
 PKIDATE_FORMAT = '%Y-%m-%d'
 
 
+def _acl_make_request(principal_type, principal, ca_id, profile_id):
+    """Construct HBAC request for the given principal, CA and profile"""
+
+    req = pyhbac.HbacRequest()
+    req.targethost.name = ca_id
+    req.service.name = profile_id
+    if principal_type == 'user':
+        req.user.name = principal.username
+    elif principal_type == 'host':
+        req.user.name = principal.hostname
+    elif principal_type == 'service':
+        req.user.name = unicode(principal)
+    groups = []
+    if principal_type == 'user':
+        user_obj = api.Command.user_show(principal.username)['result']
+        groups = user_obj.get('memberof_group', [])
+        groups += user_obj.get('memberofindirect_group', [])
+    elif principal_type == 'host':
+        host_obj = api.Command.host_show(principal.hostname)['result']
+        groups = host_obj.get('memberof_hostgroup', [])
+        groups += host_obj.get('memberofindirect_hostgroup', [])
+    req.user.groups = sorted(set(groups))
+    return req
+
+
+def _acl_make_rule(principal_type, obj):
+    """Turn CA ACL object into HBAC rule.
+
+    ``principal_type``
+        String in {'user', 'host', 'service'}
+    """
+    rule = pyhbac.HbacRule(obj['cn'][0])
+    rule.enabled = obj['ipaenabledflag'][0]
+    rule.srchosts.category = {pyhbac.HBAC_CATEGORY_ALL}
+
+    # add CA(s)
+    if 'ipacacategory' in obj and obj['ipacacategory'][0].lower() == 'all':
+        rule.targethosts.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        # For compatibility with pre-lightweight-CAs CA ACLs,
+        # no CA members implies the host authority (only)
+        rule.targethosts.names = obj.get('ipamemberca_ca', [IPA_CA_CN])
+
+    # add profiles
+    if ('ipacertprofilecategory' in obj
+            and obj['ipacertprofilecategory'][0].lower() == 'all'):
+        rule.services.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        attr = 'ipamembercertprofile_certprofile'
+        rule.services.names = obj.get(attr, [])
+
+    # add principals and principal's groups
+    category_attr = '{}category'.format(principal_type)
+    if category_attr in obj and obj[category_attr][0].lower() == 'all':
+        rule.users.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        if principal_type == 'user':
+            rule.users.names = obj.get('memberuser_user', [])
+            rule.users.groups = obj.get('memberuser_group', [])
+        elif principal_type == 'host':
+            rule.users.names = obj.get('memberhost_host', [])
+            rule.users.groups = obj.get('memberhost_hostgroup', [])
+        elif principal_type == 'service':
+            rule.users.names = [
+                unicode(principal)
+                for principal in obj.get('memberservice_service', [])
+            ]
+
+    return rule
+
+
+def acl_evaluate(principal, ca_id, profile_id):
+    if principal.is_user:
+        principal_type = 'user'
+    elif principal.is_host:
+        principal_type = 'host'
+    else:
+        principal_type = 'service'
+    req = _acl_make_request(principal_type, principal, ca_id, profile_id)
+    acls = api.Command.caacl_find(no_members=False)['result']
+    rules = [_acl_make_rule(principal_type, obj) for obj in acls]
+    return req.evaluate(rules) == pyhbac.HBAC_EVAL_ALLOW
+
+
 def normalize_pkidate(value):
     return datetime.datetime.strptime(value, PKIDATE_FORMAT)
 
diff --git a/ipaserver/plugins/hbactest.py b/ipaserver/plugins/hbactest.py
index 626e894..e156568 100644
--- a/ipaserver/plugins/hbactest.py
+++ b/ipaserver/plugins/hbactest.py
@@ -29,9 +29,14 @@
     except ImportError:
         _dcerpc_bindings_installed = False
 
-import pyhbac
 import six
 
+try:
+    import pyhbac
+except ImportError:
+    pyhbac = None
+
+
 if six.PY3:
     unicode = str
 
@@ -210,7 +215,7 @@
 
 register = Registry()
 
-def convert_to_ipa_rule(rule):
+def _convert_to_ipa_rule(rule):
     # convert a dict with a rule to an pyhbac rule
     ipa_rule = pyhbac.HbacRule(rule['cn'][0])
     ipa_rule.enabled = rule['ipaenabledflag'][0]
@@ -309,6 +314,14 @@ def canonicalize(self, host):
         return host
 
     def execute(self, *args, **options):
+        if pyhbac is None:
+            raise errors.ValidationError(
+                name=_('missing pyhbac'),
+                error=_(
+                    'pyhbac is not available on the server.'
+                )
+            )
+
         # First receive all needed information:
         # 1. HBAC rules (whether enabled or disabled)
         # 2. Required options are (user, target host, service)
@@ -356,7 +369,7 @@ def execute(self, *args, **options):
         # --disabled will import all disabled rules
         # --rules will implicitly add the rules from a rule list
         for rule in hbacset:
-            ipa_rule = convert_to_ipa_rule(rule)
+            ipa_rule = _convert_to_ipa_rule(rule)
             if ipa_rule.name in testrules:
                 ipa_rule.enabled = True
                 rules.append(ipa_rule)

From dee74240ae38f0f276dafae7d08fd5a01ad4fb46 Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Tue, 17 Jan 2017 08:57:33 +0100
Subject: [PATCH 6/8] Add extra_requires for additional dependencies

ipaserver did not have extra_requires to state additional dependencies.

Signed-off-by: Christian Heimes <chei...@redhat.com>
---
 ipaserver/setup.py | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/ipaserver/setup.py b/ipaserver/setup.py
index 42b0c1b..227327b 100755
--- a/ipaserver/setup.py
+++ b/ipaserver/setup.py
@@ -60,12 +60,6 @@
             "pyasn1",
             "pyldap",
             "six",
-            # not available on PyPI
-            # "python-libipa_hbac",
-            # "python-sss",
-            # "python-sss-murmur",
-            # "python-SSSDConfig",
-            # "samba-python",
         ],
         entry_points={
             'custodia.authorizers': [
@@ -75,4 +69,12 @@
                 'IPASecStore = ipaserver.secrets.store:IPASecStore',
             ],
         },
+        extras_require={
+            # These packages are currently not available on PyPI.
+            "caacl": ["pyhbac"],
+            "dcerpc": ["samba", "pysss", "pysss_nss_idmap"],
+            "hbactest": ["pyhbac"],
+            "install": ["SSSDConfig"],
+            "trust": ["pysss_murmur", "pysss_nss_idmap"],
+        }
     )

From bbd9384a7ee13792c8478ef40ade12d63fd76547 Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Tue, 17 Jan 2017 12:16:25 +0100
Subject: [PATCH 7/8] Add an option to build ipaserver wheels

To create a wheel bundle with ipaserver and its dependencies:

    make wheel_bundle IPA_SERVER_WHEELS=1

To include additional dependencies:

    make wheel_bundle IPA_EXTRA_WHEELS=ipatests[webui]

Signed-off-by: Christian Heimes <chei...@redhat.com>
---
 Makefile.am                   | 22 ++++++++++++++++++----
 configure.ac                  |  6 ++++++
 freeipa.spec.in               |  1 +
 ipaserver/plugins/hbactest.py | 10 +---------
 ipaserver/setup.py            |  1 -
 ipasetup.py.in                |  3 ++-
 6 files changed, 28 insertions(+), 15 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index efa8b73..1989b19 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -256,6 +256,17 @@ endif  # WITH_JSLINT
 WHEELDISTDIR = $(top_builddir)/dist/wheels
 WHEELBUNDLEDIR = $(top_builddir)/dist/bundle
 
+@MK_IFEQ@ ($(IPA_SERVER_WHEELS),1)
+    IPA_WHEEL_PACKAGES @MK_ASSIGN@ $(IPACLIENT_SUBDIRS) ipaplatform ipaserver
+    IPA_OMIT_INSTALL @MK_ASSIGN@ 0
+@MK_ELSE@
+    IPA_WHEEL_PACKAGES @MK_ASSIGN@ $(IPACLIENT_SUBDIRS)
+    IPA_OMIT_INSTALL @MK_ASSIGN@ 1
+@MK_ENDIF@
+
+# additional wheels for bundle, e.g. IPA_EXTRA_WHEELS="ipatests[webui] pylint"
+IPA_EXTRA_WHEELS=
+
 $(WHEELDISTDIR):
 	mkdir -p $(WHEELDISTDIR)
 
@@ -263,19 +274,22 @@ $(WHEELBUNDLEDIR):
 	mkdir -p $(WHEELBUNDLEDIR)
 
 bdist_wheel: $(WHEELDISTDIR)
-	for dir in $(IPACLIENT_SUBDIRS); do \
+	rm -f $(foreach item,$(IPA_WHEEL_PACKAGES) ipatests,$(WHEELDISTDIR)/$(item)-*.whl)
+	export IPA_OMIT_INSTALL=$(IPA_OMIT_INSTALL); \
+	for dir in $(IPA_WHEEL_PACKAGES) ipatests; do \
 	    $(MAKE) $(AM_MAKEFLAGS) -C $${dir} $@ || exit 1; \
 	done
 
 wheel_bundle: $(WHEELBUNDLEDIR) bdist_wheel .wheelconstraints
-	rm -f $(foreach item,$(IPACLIENT_SUBDIRS),$(WHEELBUNDLEDIR)/$(item)-*.whl)
-	$(PYTHON) -m pip wheel \
+	rm -f $(foreach item,$(IPA_WHEEL_PACKAGES) ipatests,$(WHEELBUNDLEDIR)/$(item)-*.whl)
+	@# dbus-python sometimes fails when MAKEFLAGS is set to -j2 or higher
+	MAKEFLAGS= $(PYTHON) -m pip wheel \
 	    --disable-pip-version-check \
 	    --constraint .wheelconstraints \
 	    --find-links $(WHEELDISTDIR) \
 	    --find-links $(WHEELBUNDLEDIR) \
 	    --wheel-dir $(WHEELBUNDLEDIR) \
-	    $(IPACLIENT_SUBDIRS)
+	    $(IPA_WHEEL_PACKAGES) $(IPA_EXTRA_WHEELS)
 
 wheel_placeholder: $(WHEELDISTDIR)
 	for dir in $(IPA_PLACEHOLDERS); do \
diff --git a/configure.ac b/configure.ac
index b006ccc..8f8751a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -385,6 +385,12 @@ AC_SUBST([GIT_VERSION], [IPA_GIT_VERSION])
 # used by Makefile.am for files depending on templates
 AC_SUBST([CONFIG_STATUS])
 
+# workaround for syntax clash between make and automake
+AC_SUBST([MK_IFEQ], [ifeq])
+AC_SUBST([MK_ELSE], [else])
+AC_SUBST([MK_ENDIF], [endif])
+AC_SUBST([MK_ASSIGN], [=])
+
 dnl ---------------------------------------------------------------------------
 dnl Finish
 dnl ---------------------------------------------------------------------------
diff --git a/freeipa.spec.in b/freeipa.spec.in
index a6100ec..9575176 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -146,6 +146,7 @@ BuildRequires:  python-cffi
 # Build dependencies for wheel packaging and PyPI upload
 #
 %if 0%{with_wheels}
+BuildRequires:  dbus-devel
 BuildRequires:  python2-twine
 BuildRequires:  python2-wheel
 %if 0%{?with_python3}
diff --git a/ipaserver/plugins/hbactest.py b/ipaserver/plugins/hbactest.py
index e156568..9c39b9a 100644
--- a/ipaserver/plugins/hbactest.py
+++ b/ipaserver/plugins/hbactest.py
@@ -34,7 +34,7 @@
 try:
     import pyhbac
 except ImportError:
-    pyhbac = None
+    raise errors.SkipPluginModule(reason=_('pyhbac is not installed.'))
 
 
 if six.PY3:
@@ -314,14 +314,6 @@ def canonicalize(self, host):
         return host
 
     def execute(self, *args, **options):
-        if pyhbac is None:
-            raise errors.ValidationError(
-                name=_('missing pyhbac'),
-                error=_(
-                    'pyhbac is not available on the server.'
-                )
-            )
-
         # First receive all needed information:
         # 1. HBAC rules (whether enabled or disabled)
         # 2. Required options are (user, target host, service)
diff --git a/ipaserver/setup.py b/ipaserver/setup.py
index 227327b..097508f 100755
--- a/ipaserver/setup.py
+++ b/ipaserver/setup.py
@@ -71,7 +71,6 @@
         },
         extras_require={
             # These packages are currently not available on PyPI.
-            "caacl": ["pyhbac"],
             "dcerpc": ["samba", "pysss", "pysss_nss_idmap"],
             "hbactest": ["pyhbac"],
             "install": ["SSSDConfig"],
diff --git a/ipasetup.py.in b/ipasetup.py.in
index 0940f5f..b0a5051 100644
--- a/ipasetup.py.in
+++ b/ipasetup.py.in
@@ -29,7 +29,8 @@ class build_py(setuptools_build_py):
 
     def finalize_options(self):
         setuptools_build_py.finalize_options(self)
-        if 'bdist_wheel' in self.distribution.commands:
+        omit = os.environ.get('IPA_OMIT_INSTALL', '0')
+        if omit == '1':
             distname = self.distribution.metadata.name
             self.skip_package = '{}.install'.format(distname)
             log.warn("bdist_wheel: Ignore package: %s",

From 60f2d3c05c5c44d79a1051c213dd856cea411820 Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Mon, 3 Apr 2017 10:04:04 +0200
Subject: [PATCH 8/8] Don't hard-code with_wheels

Signed-off-by: Christian Heimes <chei...@redhat.com>
---
 freeipa.spec.in | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 9575176..6ec8c68 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -31,9 +31,6 @@
     %global linter_options --disable-pylint --without-jslint
 %endif
 
-# Python wheel support and PyPI packages
-%global with_wheels 0
-
 %global alt_name ipa
 %if 0%{?rhel}
 # Require 4.6.0-4 which brings RC4 for FIPS + trust fixes to priv. separation
@@ -145,7 +142,7 @@ BuildRequires:  python-cffi
 #
 # Build dependencies for wheel packaging and PyPI upload
 #
-%if 0%{with_wheels}
+%if 0%{?with_wheels}
 BuildRequires:  dbus-devel
 BuildRequires:  python2-twine
 BuildRequires:  python2-wheel
-- 
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