[Freeipa-devel] [PATCH] 0102..0105 Better handling for cert-request to disabled CA

Fraser Tweedale ftweedal at redhat.com
Tue Sep 6 14:51:45 UTC 2016


On Tue, Aug 30, 2016 at 10:54:32AM +0200, Martin Babinsky wrote:
> On 08/26/2016 04:19 AM, Fraser Tweedale wrote:
> > The attached patches add better handling of cert-request failure due
> > to target CA being disabled (#6260).  To do this, rather than go and
> > do extra work in Dogtag that we would depend on, instead I bite the
> > bullet and refactor ra.request_certificate to use the Dogtag REST
> > API, which correctly responds with status 409 in this case.
> > 
> > Switching RA to Dogtag REST API is an old ticket (#3437) so these
> > patches address it in part, and show the way forward for the rest of
> > it.
> > 
> > These patches don't technically depend on patch 0101 which adds the
> > ca-enable and ca-disable commands, but 0101 may help for testing :)
> > 
> > Thanks,
> > Fraser
> > 
> > 
> > 
> 
> Hi Fraser,
> 
> PATCH 102:
> 
> LGTM, but please use the standard ":param " annotations in the docstring for
> `_ssldo` method. It will make out life easier if we decide to use Sphinx or
> similar tool to auto-generate documentation from sources.
> 
> You can also add ":raises:" section describing that RemoteRetrieveError is
> raised when use_session is True but the session cookie wasn't acquired. It
> is kind of obvious but it may trip the uninitiated.
> 
> PATCH 103:
> 
> Due to magical behavior of our public errors, the exception body should look
> like this:
> 
> --- a/ipalib/errors.py
> +++ b/ipalib/errors.py
> @@ -1413,10 +1413,7 @@ class HTTPRequestError(RemoteRetrieveError):
>      """
> 
>      errno = 4035
> -
> -    def __init__(self, status=None, **kw):
> -        assert status is not None
> -        super(HTTPRequestError, self).__init__(status=status, **kw)
> +    format = _('Request failed with status %(status)s: %(reason)')
> 
> The format string will be then automatically be supplied with status and
> reason if you pass them to the constructor ass you already do. The errors
> will be also handled magically (such as status which is None etc.)
> 
> PATCH 104:
> 
> 1.) please don't use bare except here:
> 
> """
> +        try:
> +            resp_obj = json.loads(http_body)
> +        except:
> +            raise errors.RemoteRetrieveError(reason=_("Response from CA was
> not valid JSON"))
> """
> 
> use 'except Exception' at least.
> 
> PATCH 105:
> 
> +            if e.status == 409:  # pylint: disable=E1101
> +                raise errors.CertificateOperationError(
> +                    error=_("CA '%s' is disabled") % ca)
> +            else:
> +                raise e
> +
> 
> please use named errors instead of error codes in pylint annotations:
> # pylint: disable=no-member
> 
Thanks for your review, Martin.  Updated patches attached; they
address all mentioned issues.

Cheers,
Fraser
-------------- next part --------------
From a1aa93ed13a24c9ac946e47ecd49606ebad8bd9e Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 26 Aug 2016 08:59:10 +1000
Subject: [PATCH 102/105] Allow Dogtag RestClient to perform requests without
 logging in

Currently the Dogtag RestClient '_ssldo' method requires a session
cookie unconditionally, however, not all REST methods require a
session: some do not require authentication at all, and some will
authenticate the agent on the fly.

To avoid unnecessary login/logout requests via the context manager,
add the 'use_session' keyword argument to '_ssldo'.  It defaults to
'True' to preserve existing behaviour (session required) but a
caller can set to 'False' to avoid the requirement.

Part of: https://fedorahosted.org/freeipa/ticket/6260
Part of: https://fedorahosted.org/freeipa/ticket/3473
---
 ipaserver/plugins/dogtag.py | 36 ++++++++++++++++++++++++------------
 1 file changed, 24 insertions(+), 12 deletions(-)

diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 01e5f1383ee135696a8e968793863ce964025094..f3fb2703f4e1ea688e38cecd02c9acc79213eb40 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -2071,26 +2071,38 @@ class RestClient(Backend):
         )
         self.cookie = None
 
-    def _ssldo(self, method, path, headers=None, body=None):
+    def _ssldo(self, method, path, headers=None, body=None, use_session=True):
         """
-        :param url: The URL to post to.
-        :param kw:  Keyword arguments to encode into POST body.
+        Perform an HTTPS request.
+
+        :param method: HTTP method to use
+        :param path: Path component. This will *extend* the path defined for
+            the class (if any).
+        :param headers: Additional headers to include in the request.
+        :param body: Request body.
+        :param use_session: If ``True``, session cookie is added to request
+            (client must be logged in).
+
         :return:   (http_status, http_headers, http_body)
                    as (integer, dict, str)
 
-        Perform an HTTPS request
-        """
-        if self.cookie is None:
-            raise errors.RemoteRetrieveError(
-                reason=_("REST API is not logged in."))
+        :raises: ``RemoteRetrieveError`` if ``use_session`` is not ``False``
+            and client is not logged in.
 
+        """
         headers = headers or {}
-        headers['Cookie'] = self.cookie
 
+        if use_session:
+            if self.cookie is None:
+                raise errors.RemoteRetrieveError(
+                    reason=_("REST API is not logged in."))
+            headers['Cookie'] = self.cookie
+
+        resource = '/ca/rest'
+        if self.path is not None:
+            resource = os.path.join(resource, self.path)
         if path is not None:
-            resource = os.path.join('/ca/rest', self.path, path)
-        else:
-            resource = os.path.join('/ca/rest', self.path)
+            resource = os.path.join(resource, path)
 
         # perform main request
         status, resp_headers, resp_body = dogtag.https_request(
-- 
2.5.5

-------------- next part --------------
From ad6b3eb1c831eb845217c8e2da590ce1dd3c2c5f Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 26 Aug 2016 09:04:04 +1000
Subject: [PATCH 103/105] Add HTTPRequestError class

Currently, HTTP requests that respond with status not in the 2xx
range raise RemoteRetrieveError.  The exception includes no
information about the response status.

Add the 'HTTPRequestError' class which extends 'RemoteRequestError'
with an attribute for the response status, and update the Dogtag
RestClient to raise the new error.

Part of: https://fedorahosted.org/freeipa/ticket/6260
Part of: https://fedorahosted.org/freeipa/ticket/3473
---
 ipalib/errors.py            | 10 ++++++++++
 ipaserver/plugins/dogtag.py |  3 ++-
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/ipalib/errors.py b/ipalib/errors.py
index 4cc4455b0abf7d2b1366e1ce6dbb3762bc551cc6..86d758b98e65b13c08d4f3a6bcb54e5a612fb3c4 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -1406,6 +1406,16 @@ class OperationNotSupportedForPrincipalType(ExecutionError):
         '%(operation)s is not supported for %(principal_type)s principals')
 
 
+class HTTPRequestError(RemoteRetrieveError):
+    """
+    **4035** Raised when an HTTP request fails.  Includes the response
+    status in the ``status`` attribute.
+    """
+
+    errno = 4035
+    format = _('Request failed with status %(status)s: %(reason)s')
+
+
 class BuiltinError(ExecutionError):
     """
     **4100** Base class for builtin execution errors (*4100 - 4199*).
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index f3fb2703f4e1ea688e38cecd02c9acc79213eb40..a7742ffa9bbe00c2f435ed457ff62f14f1d529ba 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -2113,7 +2113,8 @@ class RestClient(Backend):
         )
         if status < 200 or status >= 300:
             explanation = self._parse_dogtag_error(resp_body) or ''
-            raise errors.RemoteRetrieveError(
+            raise errors.HTTPRequestError(
+                status=status,
                 reason=_('Non-2xx response from CA REST API: %(status)d. %(explanation)s')
                 % {'status': status, 'explanation': explanation}
             )
-- 
2.5.5

-------------- next part --------------
From 2933f932c9ed8ad54531b831f22f1a15a7f1d82d Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 26 Aug 2016 10:02:21 +1000
Subject: [PATCH 104/105] Use Dogtag REST API for certificate requests

The Dogtag REST API gives better responses statuses than the RPC API
and properly reports failure due to disabled CA (status 409).  Make
'ra' extend 'RestClient' and refactor the 'request_certificate'
method to use Dogtag's REST API.

Part of: https://fedorahosted.org/freeipa/ticket/6260
Part of: https://fedorahosted.org/freeipa/ticket/3473
---
 install/conf/ipa-pki-proxy.conf |   4 +-
 ipaserver/plugins/dogtag.py     | 498 ++++++++++++++++------------------------
 2 files changed, 204 insertions(+), 298 deletions(-)

diff --git a/install/conf/ipa-pki-proxy.conf b/install/conf/ipa-pki-proxy.conf
index 545f21253ec8895397e43a3c9637956e94f40293..b48a3020d22df623fab471b2367adfd5d521544c 100644
--- a/install/conf/ipa-pki-proxy.conf
+++ b/install/conf/ipa-pki-proxy.conf
@@ -1,4 +1,4 @@
-# VERSION 9 - DO NOT REMOVE THIS LINE
+# VERSION 10 - DO NOT REMOVE THIS LINE
 
 ProxyRequests Off
 
@@ -27,7 +27,7 @@ ProxyRequests Off
 </LocationMatch>
 
 # matches for CA REST API
-<LocationMatch "^/ca/rest/account/login|^/ca/rest/account/logout|^/ca/rest/installer/installToken|^/ca/rest/securityDomain/domainInfo|^/ca/rest/securityDomain/installToken|^/ca/rest/profiles|^/ca/rest/authorities|^/ca/rest/admin/kraconnector/remove">
+<LocationMatch "^/ca/rest/account/login|^/ca/rest/account/logout|^/ca/rest/installer/installToken|^/ca/rest/securityDomain/domainInfo|^/ca/rest/securityDomain/installToken|^/ca/rest/profiles|^/ca/rest/authorities|^/ca/rest/certrequests|^/ca/rest/admin/kraconnector/remove">
     NSSOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
     NSSVerifyClient optional
     ProxyPassMatch ajp://localhost:$DOGTAG_PORT
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index a7742ffa9bbe00c2f435ed457ff62f14f1d529ba..77d24731bbc102ace3123a6fe41a631ea7c24f3b 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -566,83 +566,6 @@ def parse_error_response_xml(doc):
 
     return response
 
-def parse_profile_submit_result_xml(doc):
-    '''
-    :param doc: The root node of the xml document to parse
-    :returns:   result dict
-    :except ValueError:
-
-    CMS returns an error code and an array of request records.
-
-    This function returns a response dict with the following format:
-    {'error_code' : int, 'requests' : [{}]}
-
-    The mapping of fields and data types is illustrated in the following table.
-
-    If the error_code is not SUCCESS then the response dict will have the
-    contents described in `parse_error_response_xml`.
-
-    +--------------------+----------------+------------------------+---------------+
-    |cms name            |cms type        |result name             |result type    |
-    +====================+================+========================+===============+
-    |Status              |int             |error_code              |int            |
-    +--------------------+----------------+------------------------+---------------+
-    |Requests[].Id       |string          |requests[].request_id   |unicode        |
-    +--------------------+----------------+------------------------+---------------+
-    |Requests[].SubjectDN|string          |requests[].subject      |unicode        |
-    +--------------------+----------------+------------------------+---------------+
-    |Requests[].serialno |BigInteger      |requests[].serial_number|int|long       |
-    +--------------------+----------------+------------------------+---------------+
-    |Requests[].b64      |string          |requests[].certificate  |unicode [1]_   |
-    +--------------------+----------------+------------------------+---------------+
-    |Requests[].pkcs7    |string          |                        |               |
-    +--------------------+----------------+------------------------+---------------+
-
-    .. [1] Base64 encoded
-
-    '''
-
-    error_code = get_error_code_xml(doc)
-    if error_code != CMS_SUCCESS:
-        response = parse_error_response_xml(doc)
-        return response
-
-    response = {}
-    response['error_code'] = error_code
-
-    requests = []
-    response['requests'] = requests
-
-    for request in doc.xpath('//XMLResponse/Requests[*]/Request'):
-        response_request = {}
-        requests.append(response_request)
-
-        request_id = request.xpath('Id[1]')
-        if len(request_id) == 1:
-            request_id = etree.tostring(request_id[0], method='text',
-                                        encoding=unicode).strip()
-            response_request['request_id'] = request_id
-
-        subject_dn = request.xpath('SubjectDN[1]')
-        if len(subject_dn) == 1:
-            subject_dn = etree.tostring(subject_dn[0], method='text',
-                                        encoding=unicode).strip()
-            response_request['subject'] = subject_dn
-
-        serial_number = request.xpath('serialno[1]')
-        if len(serial_number) == 1:
-            serial_number = int(serial_number[0].text, 16) # parse as hex
-            response_request['serial_number'] = serial_number
-            response['serial_number_hex'] = u'0x%X' % serial_number
-
-        certificate = request.xpath('b64[1]')
-        if len(certificate) == 1:
-            certificate = etree.tostring(certificate[0], method='text',
-                                         encoding=unicode).strip()
-            response_request['certificate'] = certificate
-
-    return response
-
 
 def parse_check_request_result_xml(doc):
     '''
@@ -1286,32 +1209,159 @@ from ipaplatform.paths import paths
 register = Registry()
 
 
+class RestClient(Backend):
+    """Simple Dogtag REST client to be subclassed by other backends.
+
+    This class is a context manager.  Authenticated calls must be
+    executed in a ``with`` suite::
+
+        @register()
+        class ra_certprofile(RestClient):
+            path = 'profile'
+            ...
+
+        with api.Backend.ra_certprofile as profile_api:
+            # REST client is now logged in
+            profile_api.create_profile(...)
+
+    """
+    path = None
+
+    @staticmethod
+    def _parse_dogtag_error(body):
+        try:
+            return pki.PKIException.from_json(json.loads(body))
+        except Exception:
+            return None
+
+    def __init__(self, api):
+        if api.env.in_tree:
+            self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
+            self.pwd_file = self.sec_dir + os.sep + '.pwd'
+        else:
+            self.sec_dir = paths.HTTPD_ALIAS_DIR
+            self.pwd_file = paths.ALIAS_PWDFILE_TXT
+        self.noise_file = self.sec_dir + os.sep + '.noise'
+        self.ipa_key_size = "2048"
+        self.ipa_certificate_nickname = "ipaCert"
+        self.ca_certificate_nickname = "caCert"
+        self._read_password()
+        super(RestClient, self).__init__(api)
+
+        # session cookie
+        self.override_port = None
+        self.cookie = None
+
+    def _read_password(self):
+        try:
+            with open(self.pwd_file) as f:
+                self.password = f.readline().strip()
+        except IOError:
+            self.password = ''
+
+    @cachedproperty
+    def ca_host(self):
+        """
+        :return:   host
+                   as str
+
+        Select our CA host.
+        """
+        ldap2 = self.api.Backend.ldap2
+        if host_has_service(api.env.ca_host, ldap2, "CA"):
+            return api.env.ca_host
+        if api.env.host != api.env.ca_host:
+            if host_has_service(api.env.host, ldap2, "CA"):
+                return api.env.host
+        host = select_any_master(ldap2)
+        if host:
+            return host
+        else:
+            return api.env.ca_host
+
+    def __enter__(self):
+        """Log into the REST API"""
+        if self.cookie is not None:
+            return
+        status, resp_headers, resp_body = dogtag.https_request(
+            self.ca_host, self.override_port or self.env.ca_agent_port,
+            '/ca/rest/account/login',
+            self.sec_dir, self.password, self.ipa_certificate_nickname,
+            method='GET'
+        )
+        cookies = ipapython.cookie.Cookie.parse(resp_headers.get('set-cookie', ''))
+        if status != 200 or len(cookies) == 0:
+            raise errors.RemoteRetrieveError(reason=_('Failed to authenticate to CA REST API'))
+        self.cookie = str(cookies[0])
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        """Log out of the REST API"""
+        dogtag.https_request(
+            self.ca_host, self.override_port or self.env.ca_agent_port,
+            '/ca/rest/account/logout',
+            self.sec_dir, self.password, self.ipa_certificate_nickname,
+            method='GET'
+        )
+        self.cookie = None
+
+    def _ssldo(self, method, path, headers=None, body=None, use_session=True):
+        """
+        Perform an HTTPS request.
+
+        :param method: HTTP method to use
+        :param path: Path component. This will *extend* the path defined for
+            the class (if any).
+        :param headers: Additional headers to include in the request.
+        :param body: Request body.
+        :param use_session: If ``True``, session cookie is added to request
+            (client must be logged in).
+
+        :return:   (http_status, http_headers, http_body)
+                   as (integer, dict, str)
+
+        :raises: ``RemoteRetrieveError`` if ``use_session`` is not ``False``
+            and client is not logged in.
+
+        """
+        headers = headers or {}
+
+        if use_session:
+            if self.cookie is None:
+                raise errors.RemoteRetrieveError(
+                    reason=_("REST API is not logged in."))
+            headers['Cookie'] = self.cookie
+
+        resource = '/ca/rest'
+        if self.path is not None:
+            resource = os.path.join(resource, self.path)
+        if path is not None:
+            resource = os.path.join(resource, path)
+
+        # perform main request
+        status, resp_headers, resp_body = dogtag.https_request(
+            self.ca_host, self.override_port or self.env.ca_agent_port,
+            resource,
+            self.sec_dir, self.password, self.ipa_certificate_nickname,
+            method=method, headers=headers, body=body
+        )
+        if status < 200 or status >= 300:
+            explanation = self._parse_dogtag_error(resp_body) or ''
+            raise errors.HTTPRequestError(
+                status=status,
+                reason=_('Non-2xx response from CA REST API: %(status)d. %(explanation)s')
+                % {'status': status, 'explanation': explanation}
+            )
+        return (status, resp_headers, resp_body)
+
+
 @register()
-class ra(rabase.rabase):
+class ra(rabase.rabase, RestClient):
     """
     Request Authority backend plugin.
     """
     DEFAULT_PROFILE = dogtag.DEFAULT_PROFILE
 
-    def __init__(self, api):
-        if api.env.in_tree:
-            self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
-            self.pwd_file = self.sec_dir + os.sep + '.pwd'
-        else:
-            self.sec_dir = paths.HTTPD_ALIAS_DIR
-            self.pwd_file = paths.ALIAS_PWDFILE_TXT
-        self.noise_file = self.sec_dir + os.sep + '.noise'
-        self.ipa_key_size = "2048"
-        self.ipa_certificate_nickname = "ipaCert"
-        self.ca_certificate_nickname = "caCert"
-        try:
-            f = open(self.pwd_file, "r")
-            self.password = f.readline().strip()
-            f.close()
-        except IOError:
-            self.password = ''
-        super(ra, self).__init__(api)
-
     def raise_certificate_operation_error(self, func_name, err_msg=None, detail=None):
         """
         :param func_name: function name where error occurred
@@ -1564,75 +1614,77 @@ class ra(rabase.rabase):
 
         Submit certificate signing request.
 
-        The command returns a dict with these possible key/value pairs.
-        Some key/value pairs may be absent.
+        The command returns a dict with these key/value pairs:
 
-        +---------------+---------------+---------------+
-        |result name    |result type    |comments       |
-        +===============+===============+===============+
-        |serial_number  |unicode [1]_   |               |
-        +---------------+---------------+---------------+
-        |certificate    |unicode [2]_   |               |
-        +---------------+---------------+---------------+
-        |request_id     |unicode        |               |
-        +---------------+---------------+---------------+
-        |subject        |unicode        |               |
-        +---------------+---------------+---------------+
-
-        .. [1] Passed through XMLRPC as decimal string. Can convert to
-               optimal integer type (int or long) via int(serial_number)
-
-        .. [2] Base64 encoded
+        ``serial_number``
+            ``unicode``, decimal representation
+        ``serial_number_hex``
+            ``unicode``, hex representation with ``'0x'`` leader
+        ``certificate``
+            ``unicode``, base64-encoded DER
+        ``request_id``
+            ``unicode``, decimal representation
 
         """
         self.debug('%s.request_certificate()', type(self).__name__)
 
         # Call CMS
-        kw = dict(
-            profileId=profile_id,
-            cert_request_type=request_type,
-            cert_request=csr,
-            xml='true')
+        template = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+            <CertEnrollmentRequest>
+                <ProfileID>{profile}</ProfileID>
+                <Input id="i1">
+                    <ClassID>certReqInputImpl</ClassID>
+                    <Attribute name="cert_request_type">
+                        <Value>{req_type}</Value>
+                    </Attribute>
+                    <Attribute name="cert_request">
+                        <Value>{req}</Value>
+                    </Attribute>
+                </Input>
+            </CertEnrollmentRequest>'''
+        data = template.format(
+            profile=profile_id,
+            req_type=request_type,
+            req=csr,
+        )
+
+        path = 'certrequests'
         if ca_id:
-            kw['authorityId'] = ca_id
+            path += '?issuer-id={}'.format(ca_id)
 
-        http_status, http_headers, http_body = self._sslget(
-            '/ca/eeca/ca/profileSubmitSSLClient', self.env.ca_ee_port, **kw)
-        # Parse and handle errors
-        if http_status != 200:
-            self.raise_certificate_operation_error('request_certificate',
-                                                   detail=http_status)
-
-        parse_result = self.get_parse_result_xml(http_body, parse_profile_submit_result_xml)
-        # Note different status return, it's not request_status, it's error_code
-        error_code = parse_result['error_code']
-        if error_code != CMS_SUCCESS:
-            self.raise_certificate_operation_error('request_certificate',
-                                                   cms_error_code_to_string(error_code),
-                                                   parse_result.get('error_string'))
+        http_status, http_headers, http_body = self._ssldo(
+            'POST', path,
+            headers={
+                'Content-Type': 'application/xml',
+                'Accept': 'application/json',
+            },
+            body=data,
+            use_session=False,
+        )
 
+        try:
+            resp_obj = json.loads(http_body)
+        except ValueError:
+            raise errors.RemoteRetrieveError(reason=_("Response from CA was not valid JSON"))
 
         # Return command result
         cmd_result = {}
 
-        # FIXME: should we return all the requests instead of just the first one?
-        if len(parse_result['requests']) < 1:
+        entries = resp_obj.get('entries', [])
+
+        # ipa cert-request only handles a single PKCS #10 request so
+        # there's only one certinfo in the result.
+        if len(entries) < 1:
             return cmd_result
-        request = parse_result['requests'][0]
+        certinfo = entries[0]
 
-        if 'serial_number' in request:
-            # see module documentation concerning serial numbers and XMLRPC
-            cmd_result['serial_number'] = unicode(request['serial_number'])
-            cmd_result['serial_number_hex'] = u'0x%X' % request['serial_number']
+        if 'certId' in certinfo:
+            cmd_result = self.get_certificate(certinfo['certId'])
+            cert = ''.join(cmd_result['certificate'].splitlines())
+            cmd_result['certificate'] = cert
 
-        if 'certificate' in request:
-            cmd_result['certificate'] = request['certificate']
-
-        if 'request_id' in request:
-            cmd_result['request_id'] = request['request_id']
-
-        if 'subject' in request:
-            cmd_result['subject'] = request['subject']
+        if 'requestURL' in certinfo:
+            cmd_result['request_id'] = certinfo['requestURL'].split('/')[-1]
 
         return cmd_result
 
@@ -1975,152 +2027,6 @@ class kra(Backend):
         return KRAClient(connection, crypto)
 
 
-class RestClient(Backend):
-    """Simple Dogtag REST client to be subclassed by other backends.
-
-    This class is a context manager.  Authenticated calls must be
-    executed in a ``with`` suite::
-
-        @register()
-        class ra_certprofile(RestClient):
-            path = 'profile'
-            ...
-
-        with api.Backend.ra_certprofile as profile_api:
-            # REST client is now logged in
-            profile_api.create_profile(...)
-
-    """
-    path = None
-
-    @staticmethod
-    def _parse_dogtag_error(body):
-        try:
-            return pki.PKIException.from_json(json.loads(body))
-        except Exception:
-            return None
-
-    def __init__(self, api):
-        if api.env.in_tree:
-            self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
-            self.pwd_file = self.sec_dir + os.sep + '.pwd'
-        else:
-            self.sec_dir = paths.HTTPD_ALIAS_DIR
-            self.pwd_file = paths.ALIAS_PWDFILE_TXT
-        self.noise_file = self.sec_dir + os.sep + '.noise'
-        self.ipa_key_size = "2048"
-        self.ipa_certificate_nickname = "ipaCert"
-        self.ca_certificate_nickname = "caCert"
-        self._read_password()
-        super(RestClient, self).__init__(api)
-
-        # session cookie
-        self.override_port = None
-        self.cookie = None
-
-    def _read_password(self):
-        try:
-            with open(self.pwd_file) as f:
-                self.password = f.readline().strip()
-        except IOError:
-            self.password = ''
-
-    @cachedproperty
-    def ca_host(self):
-        """
-        :return:   host
-                   as str
-
-        Select our CA host.
-        """
-        ldap2 = self.api.Backend.ldap2
-        if host_has_service(api.env.ca_host, ldap2, "CA"):
-            return api.env.ca_host
-        if api.env.host != api.env.ca_host:
-            if host_has_service(api.env.host, ldap2, "CA"):
-                return api.env.host
-        host = select_any_master(ldap2)
-        if host:
-            return host
-        else:
-            return api.env.ca_host
-
-    def __enter__(self):
-        """Log into the REST API"""
-        if self.cookie is not None:
-            return
-        status, resp_headers, resp_body = dogtag.https_request(
-            self.ca_host, self.override_port or self.env.ca_agent_port,
-            '/ca/rest/account/login',
-            self.sec_dir, self.password, self.ipa_certificate_nickname,
-            method='GET'
-        )
-        cookies = ipapython.cookie.Cookie.parse(resp_headers.get('set-cookie', ''))
-        if status != 200 or len(cookies) == 0:
-            raise errors.RemoteRetrieveError(reason=_('Failed to authenticate to CA REST API'))
-        self.cookie = str(cookies[0])
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        """Log out of the REST API"""
-        dogtag.https_request(
-            self.ca_host, self.override_port or self.env.ca_agent_port,
-            '/ca/rest/account/logout',
-            self.sec_dir, self.password, self.ipa_certificate_nickname,
-            method='GET'
-        )
-        self.cookie = None
-
-    def _ssldo(self, method, path, headers=None, body=None, use_session=True):
-        """
-        Perform an HTTPS request.
-
-        :param method: HTTP method to use
-        :param path: Path component. This will *extend* the path defined for
-            the class (if any).
-        :param headers: Additional headers to include in the request.
-        :param body: Request body.
-        :param use_session: If ``True``, session cookie is added to request
-            (client must be logged in).
-
-        :return:   (http_status, http_headers, http_body)
-                   as (integer, dict, str)
-
-        :raises: ``RemoteRetrieveError`` if ``use_session`` is not ``False``
-            and client is not logged in.
-
-        """
-        headers = headers or {}
-
-        if use_session:
-            if self.cookie is None:
-                raise errors.RemoteRetrieveError(
-                    reason=_("REST API is not logged in."))
-            headers['Cookie'] = self.cookie
-
-        resource = '/ca/rest'
-        if self.path is not None:
-            resource = os.path.join(resource, self.path)
-        if path is not None:
-            resource = os.path.join(resource, path)
-
-        # perform main request
-        status, resp_headers, resp_body = dogtag.https_request(
-            self.ca_host, self.override_port or self.env.ca_agent_port,
-            resource,
-            self.sec_dir, self.password, self.ipa_certificate_nickname,
-            method=method, headers=headers, body=body
-        )
-        if status < 200 or status >= 300:
-            explanation = self._parse_dogtag_error(resp_body) or ''
-            raise errors.HTTPRequestError(
-                status=status,
-                reason=_('Non-2xx response from CA REST API: %(status)d. %(explanation)s')
-                % {'status': status, 'explanation': explanation}
-            )
-        return (status, resp_headers, resp_body)
-
-
 @register()
 class ra_certprofile(RestClient):
     """
-- 
2.5.5

-------------- next part --------------
From 432d3b7204bb2aefa0ca2f0b56f3ca87acc2bd52 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 26 Aug 2016 11:11:56 +1000
Subject: [PATCH 105/105] cert-request: raise CertificateOperationError if CA
 disabled

Detect when cert-request returns HTTP 409, which indicates that the
target CA is disabled - a valid scenario - and raise
CertificateOperationError with a friendly message instead of
HTTPRequestError.

Fixes: https://fedorahosted.org/freeipa/ticket/6260
---
 ipaserver/plugins/cert.py | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 6195a6b1e636f35de114c5fefbe84fa3b3f116f0..a1e4aeb4ea528f592bded77983c7972ce7fb92c6 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -749,8 +749,16 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
                     info=_("Subject alt name type %s is forbidden") % desc)
 
         # Request the certificate
-        result = self.Backend.ra.request_certificate(
-            csr, profile_id, ca_id, request_type=request_type)
+        try:
+            result = self.Backend.ra.request_certificate(
+                csr, profile_id, ca_id, request_type=request_type)
+        except errors.HTTPRequestError as e:
+            if e.status == 409:  # pylint: disable=no-member
+                raise errors.CertificateOperationError(
+                    error=_("CA '%s' is disabled") % ca)
+            else:
+                raise e
+
         if not raw:
             self.obj._parse(result, all)
             result['request_id'] = int(result['request_id'])
-- 
2.5.5



More information about the Freeipa-devel mailing list