URL: https://github.com/freeipa/freeipa/pull/434
Author: LiptonB
 Title: #434: csrgen: Automate full cert request flow
Action: opened

PR body:
"""
Adds `--autogenerate` flag to `ipa cert-request` command. It no longer
requires a CSR passed on the command line, instead it creates a config
(bash script) with `cert-get-requestdata`, then runs it to build a CSR,
and submits that CSR.

Example usage (NSS database):
$ ipa cert-request --autogenerate --principal blipton --profile-id userCert 
--database /tmp/certs

Example usage (PEM private key file):
$ ipa cert-request --autogenerate --principal blipton --profile-id userCert 
--private-key /tmp/key.pem
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/434/head:pr434
git checkout pr434
From 8660c571d93b20847d59a6c40f1764ffbda13e5f Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 22 Aug 2016 10:46:02 -0400
Subject: [PATCH 1/2] csrgen: Automate full cert request flow

Adds `--autogenerate` flag to `ipa cert-request` command. It no longer
requires a CSR passed on the command line, instead it creates a config
(bash script) with `cert-get-requestdata`, then runs it to build a CSR,
and submits that CSR.

Example usage (NSS database):
$ ipa cert-request --autogenerate --principal blipton --profile-id userCert --database /tmp/certs

Example usage (PEM private key file):
$ ipa cert-request --autogenerate --principal blipton --profile-id userCert --private-key /tmp/key.pem

https://fedorahosted.org/freeipa/ticket/4899
---
 API.txt                   |  2 +-
 ipaclient/plugins/cert.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++-
 ipaserver/plugins/cert.py |  7 ++--
 3 files changed, 95 insertions(+), 4 deletions(-)

diff --git a/API.txt b/API.txt
index 543cec5..ac38514 100644
--- a/API.txt
+++ b/API.txt
@@ -788,7 +788,7 @@ option: Flag('add', autofill=True, default=False)
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa')
 option: Principal('principal')
-option: Str('profile_id?')
+option: Str('profile_id', autofill=True, default=u'caIPAserviceCert')
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Str('request_type', autofill=True, default=u'pkcs10')
 option: Str('version?')
diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 1075972..bc29378 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -19,6 +19,10 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import os
+import subprocess
+import tempfile
+
 from ipaclient.frontend import MethodOverride
 from ipalib import errors
 from ipalib import x509
@@ -32,12 +36,96 @@
 
 @register(override=True, no_fail=True)
 class cert_request(MethodOverride):
+    takes_options = (
+        Str(
+            'database?',
+            label=_('Path to NSS database'),
+            doc=_('Path to NSS database to use for private key'),
+        ),
+        Str(
+            'private_key?',
+            label=_('Path to private key file'),
+            doc=_('Path to PEM file containing a private key'),
+        ),
+        Flag(
+            'autogenerate',
+            label=_('Automatically generate CSR for request using LDAP data'),
+        )
+    )
+
     def get_args(self):
         for arg in super(cert_request, self).get_args():
             if arg.name == 'csr':
-                arg = arg.clone_retype(arg.name, File)
+                arg = arg.clone_retype(arg.name, File, required=False)
             yield arg
 
+    def forward(self, *keys, **options):
+        autogenerate = options.pop('autogenerate', False)
+        database = options.pop('database', None)
+        private_key = options.pop('private_key', None)
+
+        if autogenerate:
+            if database:
+                helper = u'certutil'
+                helper_args = ['-d', database]
+            elif private_key:
+                helper = u'openssl'
+                helper_args = [private_key]
+            else:
+                raise errors.InvocationError(
+                    format="One of 'database' or 'private_key' is required")
+
+            scriptfile = tempfile.NamedTemporaryFile(delete=False)
+            scriptfile.close()
+            csrfile = tempfile.NamedTemporaryFile(delete=False)
+            csrfile.close()
+            csrfilename = csrfile.name
+
+            # profile_id is optional for cert_request, but not for
+            # cert_get_requestdata, so pass the default explicitly when
+            # necessary
+            profile_id = options.get('profile_id')
+            if profile_id is None:
+                profile_id = self.get_default_of('profile_id')
+
+            requestdata = 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, csrfilename] + helper_args
+
+            try:
+                subprocess.check_call(helper_cmd)
+            except subprocess.CalledProcessError:
+                raise errors.CertificateOperationError(
+                    error=(
+                        _('Error running "%(cmd)s" to generate CSR') %
+                        {'cmd': ' '.join(helper_cmd)}))
+            finally:
+                os.remove(scriptfile.name)
+
+            try:
+                with open(csrfilename) as csrfile:
+                    csr = csrfile.read()
+            except IOError:
+                raise errors.CertificateOperationError(
+                    error=(_('Unable to read generated CSR file')))
+            if not csr:
+                raise errors.CertificateOperationError(
+                    error=(_('Generated CSR was empty')))
+
+            rv = super(cert_request, self).forward(unicode(csr), **options)
+            os.remove(csrfilename)
+            return rv
+        else:
+            if database is not None or private_key is not None:
+                raise errors.OptionError(format=_(
+                    "Options 'database' and 'private_key' are only valid with"
+                    " 'autogenerate'"))
+            return super(cert_request, self).forward(*keys, **options)
+
 
 @register(override=True, no_fail=True)
 class cert_show(MethodOverride):
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 5bf4cfb..a6ee18b 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -47,6 +47,7 @@
 from ipalib.text import _
 from ipalib.request import context
 from ipalib import output
+from ipapython import dogtag
 from ipapython import kerberos
 from ipapython.dn import DN
 from ipapython.ipa_log_manager import root_logger
@@ -478,7 +479,9 @@ class certreq(BaseCertObject):
             flags={'no_update', 'no_update', 'no_search'},
         ),
         Str(
-            'profile_id?', validate_profile_id,
+            'profile_id', validate_profile_id,
+            default=dogtag.DEFAULT_PROFILE,
+            autofill=True,
             label=_("Profile ID"),
             doc=_("Certificate Profile to use"),
             flags={'no_update', 'no_update', 'no_search'},
@@ -544,7 +547,7 @@ def execute(self, csr, all=False, raw=False, **kw):
         realm = unicode(self.api.env.realm)
         add = kw.get('add')
         request_type = kw.get('request_type')
-        profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE)
+        profile_id = kw.get('profile_id')
 
         # Check that requested authority exists (done before CA ACL
         # enforcement so that user gets better error message if

From 5e1ab64c299e1986ba27fd65760dcd15a2880b50 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Sat, 4 Feb 2017 10:25:42 -0500
Subject: [PATCH 2/2] csrgen: Allow overriding the CSR generation profile

In case users want multiple CSR generation profiles that work with the
same dogtag profile, or in case the profiles are not named the same,
this flag allows specifying an alternative CSR generation profile.

https://fedorahosted.org/freeipa/ticket/4899
---
 ipaclient/plugins/cert.py | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index bc29378..74d54fa 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -50,7 +50,11 @@ class cert_request(MethodOverride):
         Flag(
             'autogenerate',
             label=_('Automatically generate CSR for request using LDAP data'),
-        )
+        ),
+        Str(
+            'csr_profile_id?',
+            label=_('Name of CSR generation profile (if not the same as profile_id)'),
+        ),
     )
 
     def get_args(self):
@@ -63,6 +67,7 @@ def forward(self, *keys, **options):
         autogenerate = options.pop('autogenerate', False)
         database = options.pop('database', None)
         private_key = options.pop('private_key', None)
+        csr_profile_id = options.pop('csr_profile_id', None)
 
         if autogenerate:
             if database:
@@ -81,10 +86,13 @@ def forward(self, *keys, **options):
             csrfile.close()
             csrfilename = csrfile.name
 
-            # profile_id is optional for cert_request, but not for
-            # cert_get_requestdata, so pass the default explicitly when
-            # necessary
-            profile_id = options.get('profile_id')
+            # If csr_profile_id is passed, that takes precedence. Otherwise,
+            # use profile_id. And if neither is passed, we need the default
+            # profile_id so we can pass it explicitly to cert_get_requestdata
+            # which has no default profile.
+            profile_id = csr_profile_id
+            if profile_id is None:
+                profile_id = options.get('profile_id')
             if profile_id is None:
                 profile_id = self.get_default_of('profile_id')
 
-- 
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