diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py index ff4d89083..1c2c51824 100644 --- a/ipaserver/plugins/dogtag.py +++ b/ipaserver/plugins/dogtag.py @@ -35,6 +35,14 @@ Overview of interacting with CMS: CMS stands for "Certificate Management System". It has been released under a variety of names, the open source version is called "dogtag". +IPA now uses the REST API provided by dogtag, as documented at +https://github.com/dogtagpki/pki/wiki/REST-API + +The below is still relevant in places, particularly with data handling. +This history of Javascript parsing and using the optional XML is left +for historical purposes and for the last-used xml-based call that +IPA makes (updateCRL). + CMS consists of a number of servlets which in rough terms can be thought of as RPC commands. A servlet is invoked by making an HTTP request to a specific URL and passing URL arguments. Normally CMS responds with an HTTP response consisting @@ -298,92 +306,6 @@ def cms_request_status_to_string(request_status): 7 : 'EXCEPTION', }.get(request_status, "unknown(%d)" % request_status) -def cms_error_code_to_string(error_code): - ''' - :param error_code: The integral error code value - :return: String name of the error code - ''' - return { - 0 : 'SUCCESS', - 1 : 'FAILURE', - 2 : 'AUTH_FAILURE', - }.get(error_code, "unknown(%d)" % error_code) - -def parse_and_set_boolean_xml(node, response, response_name): - ''' - :param node: xml node object containing value to parse for boolean result - :param response: response dict to set boolean result in - :param response_name: name of the response value to set - :except ValueError: - - Read the value out of a xml text node and interpret it as a boolean value. - The text values are stripped of whitespace and converted to lower case - prior to interpretation. - - If the value is recognized the response dict is updated using the - request_name as the key and the value is set to the bool value of either - True or False depending on the interpretation of the text value. If the text - value is not recognized a ValueError exception is thrown. - - Text values which result in True: - - - true - - yes - - on - - Text values which result in False: - - - false - - no - - off - ''' - value = node.text.strip().lower() - if value in ('true', 'yes'): - value = True - elif value in ('false', 'no'): - value = False - else: - raise ValueError('expected true|false|yes|no|on|off for "%s", but got "%s"' % \ - (response_name, value)) - response[response_name] = value - -def get_error_code_xml(doc): - ''' - :param doc: The root node of the xml document to parse - :returns: error code as an integer or None if not found - - Returns the error code when the servlet replied with - CMSServlet.outputError() - - The possible error code values are: - - - CMS_SUCCESS = 0 - - CMS_FAILURE = 1 - - CMS_AUTH_FAILURE = 2 - - However, profileSubmit sometimes also returns these values: - - - EXCEPTION = 1 - - DEFERRED = 2 - - REJECTED = 3 - - ''' - - error_code = doc.xpath('//XMLResponse/Status[1]') - if len(error_code) == 1: - error_code = int(error_code[0].text) - else: - # If error code wasn't present, but error string was - # then it's an error. - error_string = doc.xpath('//XMLResponse/Error[1]') - if len(error_string) == 1: - error_code = CMS_FAILURE - else: - # no status and no error string, assume success - error_code = CMS_SUCCESS - - return error_code - def get_request_status_xml(doc): ''' :param doc: The root node of the xml document to parse @@ -513,529 +435,6 @@ def parse_error_template_xml(doc): return response -def parse_error_response_xml(doc): - ''' - :param doc: The root node of the xml document to parse - :returns: result dict - - CMS currently returns errors via XML as either a "template" document - (generated by CMSServlet.outputXML() or a "response" document (generated by - CMSServlet.outputError()). - - This routine is used to parse a "response" style error document. - - +---------------+---------------+---------------+---------------+ - |cms name |cms type |result name |result type | - +===============+===============+===============+===============+ - |Status |int |error_code |int [1]_ | - +---------------+---------------+---------------+---------------+ - |Error |string |error_string |unicode | - +---------------+---------------+---------------+---------------+ - |RequestID |string |request_id |string | - +---------------+---------------+---------------+---------------+ - - .. [1] error code may be one of: - - - CMS_SUCCESS = 0 - - CMS_FAILURE = 1 - - CMS_AUTH_FAILURE = 2 - - However, profileSubmit sometimes also returns these values: - - - EXCEPTION = 1 - - DEFERRED = 2 - - REJECTED = 3 - - ''' - - response = {} - response['error_code'] = CMS_FAILURE # assume error - - error_code = doc.xpath('//XMLResponse/Status[1]') - if len(error_code) == 1: - error_code = int(error_code[0].text) - response['error_code'] = error_code - - error_string = doc.xpath('//XMLResponse/Error[1]') - if len(error_string) == 1: - error_string = etree.tostring(error_string[0], method='text', - encoding=unicode).strip() - response['error_string'] = error_string - - request_id = doc.xpath('//XMLResponse/RequestId[1]') - if len(request_id) == 1: - request_id = etree.tostring(request_id[0], method='text', - encoding=unicode).strip() - response['request_id'] = request_id - - return response - - -def parse_check_request_result_xml(doc): - ''' - :param doc: The root node of the xml document to parse - :returns: result dict - :except ValueError: - - After parsing the results are returned in a result dict. The following - table illustrates the mapping from the CMS data item to what may be found in - the result dict. If a CMS data item is absent it will also be absent in the - result dict. - - If the requestStatus is not SUCCESS then the response dict will have the - contents described in `parse_error_template_xml`. - - +-------------------------+---------------+-------------------+-----------------+ - |cms name |cms type |result name |result type | - +=========================+===============+===================+=================+ - |authority |string |authority |unicode | - +-------------------------+---------------+-------------------+-----------------+ - |requestId |string |request_id |string | - +-------------------------+---------------+-------------------+-----------------+ - |status |string |cert_request_status|unicode [1]_ | - +-------------------------+---------------+-------------------+-----------------+ - |createdOn |long, timestamp|created_on |datetime.datetime| - +-------------------------+---------------+-------------------+-----------------+ - |updatedOn |long, timestamp|updated_on |datetime.datetime| - +-------------------------+---------------+-------------------+-----------------+ - |requestNotes |string |request_notes |unicode | - +-------------------------+---------------+-------------------+-----------------+ - |pkcs7ChainBase64 |string |pkcs7_chain |unicode [2]_ | - +-------------------------+---------------+-------------------+-----------------+ - |cmcFullEnrollmentResponse|string |full_response |unicode [2]_ | - +-------------------------+---------------+-------------------+-----------------+ - |records[].serialNumber |BigInteger |serial_numbers |[int|long] | - +-------------------------+---------------+-------------------+-----------------+ - - .. [1] cert_request_status may be one of: - - - "begin" - - "pending" - - "approved" - - "svc_pending" - - "canceled" - - "rejected" - - "complete" - - .. [2] Base64 encoded - - ''' - request_status = get_request_status_xml(doc) - - if request_status != CMS_STATUS_SUCCESS: - response = parse_error_template_xml(doc) - return response - - response = {} - response['request_status'] = request_status - - cert_request_status = doc.xpath('//xml/header/status[1]') - if len(cert_request_status) == 1: - cert_request_status = etree.tostring(cert_request_status[0], method='text', - encoding=unicode).strip() - response['cert_request_status'] = cert_request_status - - request_id = doc.xpath('//xml/header/requestId[1]') - if len(request_id) == 1: - request_id = etree.tostring(request_id[0], method='text', - encoding=unicode).strip() - response['request_id'] = request_id - - authority = doc.xpath('//xml/header/authority[1]') - if len(authority) == 1: - authority = etree.tostring(authority[0], method='text', - encoding=unicode).strip() - response['authority'] = authority - - updated_on = doc.xpath('//xml/header/updatedOn[1]') - if len(updated_on) == 1: - updated_on = ipautil.datetime_from_utctimestamp( - int(updated_on[0].text), units=1) - response['updated_on'] = updated_on - - created_on = doc.xpath('//xml/header/createdOn[1]') - if len(created_on) == 1: - created_on = ipautil.datetime_from_utctimestamp( - int(created_on[0].text), units=1) - response['created_on'] = created_on - - request_notes = doc.xpath('//xml/header/requestNotes[1]') - if len(request_notes) == 1: - request_notes = etree.tostring(request_notes[0], method='text', - encoding=unicode).strip() - response['request_notes'] = request_notes - - pkcs7_chain = doc.xpath('//xml/header/pkcs7ChainBase64[1]') - if len(pkcs7_chain) == 1: - pkcs7_chain = etree.tostring(pkcs7_chain[0], method='text', - encoding=unicode).strip() - response['pkcs7_chain'] = pkcs7_chain - - full_response = doc.xpath('//xml/header/cmcFullEnrollmentResponse[1]') - if len(full_response) == 1: - full_response = etree.tostring(full_response[0], method='text', - encoding=unicode).strip() - response['full_response'] = full_response - - serial_numbers = [] - response['serial_numbers'] = serial_numbers - for serial_number in doc.xpath('//xml/records[*]/record/serialNumber'): - serial_number = int(serial_number.text, 16) # parse as hex - serial_numbers.append(serial_number) - - return response - - -def parse_revoke_cert_xml(doc): - ''' - :param doc: The root node of the xml document to parse - :returns: result dict - :except ValueError: - - After parsing the results are returned in a result dict. The following - table illustrates the mapping from the CMS data item to what may be found in - the result dict. If a CMS data item is absent it will also be absent in the - result dict. - - If the requestStatus is not SUCCESS then the response dict will have the - contents described in `parse_error_template_xml`. - - +----------------------+----------------+-----------------------+---------------+ - |cms name |cms type |result name |result type | - +======================+================+=======================+===============+ - |dirEnabled |string [1]_ |dir_enabled |bool | - +----------------------+----------------+-----------------------+---------------+ - |certsUpdated |int |certs_updated |int | - +----------------------+----------------+-----------------------+---------------+ - |certsToUpdate |int |certs_to_update |int | - +----------------------+----------------+-----------------------+---------------+ - |error |string [2]_ |error_string |unicode | - +----------------------+----------------+-----------------------+---------------+ - |revoked |string [3]_ |revoked |unicode | - +----------------------+----------------+-----------------------+---------------+ - |totalRecordCount |int |total_record_count |int | - +----------------------+----------------+-----------------------+---------------+ - |updateCRL |string [1]_ [4]_|update_crl |bool | - +----------------------+----------------+-----------------------+---------------+ - |updateCRLSuccess |string [1]_ [4]_|update_crl_success |bool | - +----------------------+----------------+-----------------------+---------------+ - |updateCRLError |string [4]_ |update_crl_error |unicode | - +----------------------+----------------+-----------------------+---------------+ - |publishCRLSuccess |string [1]_[4]_ |publish_crl_success |bool | - +----------------------+----------------+-----------------------+---------------+ - |publishCRLError |string [4]_ |publish_crl_error |unicode | - +----------------------+----------------+-----------------------+---------------+ - |crlUpdateStatus |string [1]_ [5]_|crl_update_status |bool | - +----------------------+----------------+-----------------------+---------------+ - |crlUpdateError |string [5]_ |crl_update_error |unicode | - +----------------------+----------------+-----------------------+---------------+ - |crlPublishStatus |string [1]_ [5]_|crl_publish_status |bool | - +----------------------+----------------+-----------------------+---------------+ - |crlPublishError |string [5]_ |crl_publish_error |unicode | - +----------------------+----------------+-----------------------+---------------+ - |records[].serialNumber|BigInteger |records[].serial_number|int|long | - +----------------------+----------------+-----------------------+---------------+ - |records[].error |string [2]_ |records[].error_string |unicode | - +----------------------+----------------+-----------------------+---------------+ - - .. [1] String value is either "yes" or "no" - .. [2] Sometimes the error string is empty (null) - .. [3] revoked may be one of: - - - "yes" - - "no" - - "begin" - - "pending" - - "approved" - - "svc_pending" - - "canceled" - - "rejected" - - "complete" - - .. [4] Only sent if CRL update information is available. - If sent it's only value is "yes". - If sent then the following values may also be sent, - otherwise they will be absent: - - - updateCRLSuccess - - updateCRLError - - publishCRLSuccess - - publishCRLError - - .. [5] The cms name varies depending on whether the issuing point is MasterCRL - or not. If the issuing point is not the MasterCRL then the cms name - will be appended with an underscore and the issuing point name. - Thus for example the cms name crlUpdateStatus will be crlUpdateStatus - if the issuing point is the MasterCRL. However if the issuing point - is "foobar" then crlUpdateStatus will be crlUpdateStatus_foobar. - When we return the response dict the key will always be the "base" - name without the _issuing_point suffix. Thus crlUpdateStatus_foobar - will appear in the response dict under the key 'crl_update_status' - - ''' - - request_status = get_request_status_xml(doc) - - if request_status != CMS_STATUS_SUCCESS: - response = parse_error_template_xml(doc) - return response - - response = {} - response['request_status'] = request_status - - records = [] - response['records'] = records - - dir_enabled = doc.xpath('//xml/header/dirEnabled[1]') - if len(dir_enabled) == 1: - parse_and_set_boolean_xml(dir_enabled[0], response, 'dir_enabled') - - certs_updated = doc.xpath('//xml/header/certsUpdated[1]') - if len(certs_updated) == 1: - certs_updated = int(certs_updated[0].text) - response['certs_updated'] = certs_updated - - certs_to_update = doc.xpath('//xml/header/certsToUpdate[1]') - if len(certs_to_update) == 1: - certs_to_update = int(certs_to_update[0].text) - response['certs_to_update'] = certs_to_update - - error_string = doc.xpath('//xml/header/error[1]') - if len(error_string) == 1: - error_string = etree.tostring(error_string[0], method='text', - encoding=unicode).strip() - response['error_string'] = error_string - - revoked = doc.xpath('//xml/header/revoked[1]') - if len(revoked) == 1: - revoked = etree.tostring(revoked[0], method='text', - encoding=unicode).strip() - response['revoked'] = revoked - - total_record_count = doc.xpath('//xml/header/totalRecordCount[1]') - if len(total_record_count) == 1: - total_record_count = int(total_record_count[0].text) - response['total_record_count'] = total_record_count - - update_crl = doc.xpath('//xml/header/updateCRL[1]') - if len(update_crl) == 1: - parse_and_set_boolean_xml(update_crl[0], response, 'update_crl') - - update_crl_success = doc.xpath('//xml/header/updateCRLSuccess[1]') - if len(update_crl_success) == 1: - parse_and_set_boolean_xml(update_crl_success[0], response, 'update_crl_success') - - update_crl_error = doc.xpath('//xml/header/updateCRLError[1]') - if len(update_crl_error) == 1: - update_crl_error = etree.tostring(update_crl_error[0], method='text', - encoding=unicode).strip() - response['update_crl_error'] = update_crl_error - - publish_crl_success = doc.xpath('//xml/header/publishCRLSuccess[1]') - if len(publish_crl_success) == 1: - parse_and_set_boolean_xml(publish_crl_success[0], response, 'publish_crl_success') - - publish_crl_error = doc.xpath('//xml/header/publishCRLError[1]') - if len(publish_crl_error) == 1: - publish_crl_error = etree.tostring(publish_crl_error[0], method='text', - encoding=unicode).strip() - response['publish_crl_error'] = publish_crl_error - - crl_update_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateStatus')][1]") - if len(crl_update_status) == 1: - parse_and_set_boolean_xml(crl_update_status[0], response, 'crl_update_status') - - crl_update_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateError')][1]") - if len(crl_update_error) == 1: - crl_update_error = etree.tostring(crl_update_error[0], method='text', - encoding=unicode).strip() - response['crl_update_error'] = crl_update_error - - crl_publish_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishStatus')][1]") - if len(crl_publish_status) == 1: - parse_and_set_boolean_xml(crl_publish_status[0], response, 'crl_publish_status') - - crl_publish_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishError')][1]") - if len(crl_publish_error) == 1: - crl_publish_error = etree.tostring(crl_publish_error[0], method='text', - encoding=unicode).strip() - response['crl_publish_error'] = crl_publish_error - - for record in doc.xpath('//xml/records[*]/record'): - response_record = {} - records.append(response_record) - - serial_number = record.xpath('serialNumber[1]') - if len(serial_number) == 1: - serial_number = int(serial_number[0].text, 16) # parse as hex - response_record['serial_number'] = serial_number - response['serial_number_hex'] = u'0x%X' % serial_number - - error_string = record.xpath('error[1]') - if len(error_string) == 1: - error_string = etree.tostring(error_string[0], method='text', - encoding=unicode).strip() - response_record['error_string'] = error_string - - return response - -def parse_unrevoke_cert_xml(doc): - ''' - :param doc: The root node of the xml document to parse - :returns: result dict - :except ValueError: - - After parsing the results are returned in a result dict. The following - table illustrates the mapping from the CMS data item to what may be found in - the result dict. If a CMS data item is absent it will also be absent in the - result dict. - - If the requestStatus is not SUCCESS then the response dict will have the - contents described in `parse_error_template_xml`. - - +----------------------+----------------+-----------------------+---------------+ - |cms name |cms type |result name |result type | - +======================+================+=======================+===============+ - |dirEnabled |string [1]_ |dir_enabled |bool | - +----------------------+----------------+-----------------------+---------------+ - |dirUpdated |string [1]_ |dir_updated |bool | - +----------------------+----------------+-----------------------+---------------+ - |error |string |error_string |unicode | - +----------------------+----------------+-----------------------+---------------+ - |unrevoked |string [3]_ |unrevoked |unicode | - +----------------------+----------------+-----------------------+---------------+ - |updateCRL |string [1]_ [4]_|update_crl |bool | - +----------------------+----------------+-----------------------+---------------+ - |updateCRLSuccess |string [1]_ [4]_|update_crl_success |bool | - +----------------------+----------------+-----------------------+---------------+ - |updateCRLError |string [4]_ |update_crl_error |unicode | - +----------------------+----------------+-----------------------+---------------+ - |publishCRLSuccess |string [1]_ [4]_|publish_crl_success |bool | - +----------------------+----------------+-----------------------+---------------+ - |publishCRLError |string [4]_ |publish_crl_error |unicode | - +----------------------+----------------+-----------------------+---------------+ - |crlUpdateStatus |string [1]_ [5]_|crl_update_status |bool | - +----------------------+----------------+-----------------------+---------------+ - |crlUpdateError |string [5]_ |crl_update_error |unicode | - +----------------------+----------------+-----------------------+---------------+ - |crlPublishStatus |string [1]_ [5]_|crl_publish_status |bool | - +----------------------+----------------+-----------------------+---------------+ - |crlPublishError |string [5]_ |crl_publish_error |unicode | - +----------------------+----------------+-----------------------+---------------+ - |serialNumber |BigInteger |serial_number |int|long | - +----------------------+----------------+-----------------------+---------------+ - - .. [1] String value is either "yes" or "no" - .. [3] unrevoked may be one of: - - - "yes" - - "no" - - "pending" - - .. [4] Only sent if CRL update information is available. - If sent it's only value is "yes". - If sent then the following values may also be sent, - otherwise they will be absent: - - - updateCRLSuccess - - updateCRLError - - publishCRLSuccess - - publishCRLError - - .. [5] The cms name varies depending on whether the issuing point is MasterCRL - or not. If the issuing point is not the MasterCRL then the cms name - will be appended with an underscore and the issuing point name. - Thus for example the cms name crlUpdateStatus will be crlUpdateStatus - if the issuing point is the MasterCRL. However if the issuing point - is "foobar" then crlUpdateStatus will be crlUpdateStatus_foobar. - When we return the response dict the key will always be the "base" - name without the _issuing_point suffix. Thus crlUpdateStatus_foobar - will appear in the response dict under the key 'crl_update_status' - - ''' - - request_status = get_request_status_xml(doc) - - if request_status != CMS_STATUS_SUCCESS: - response = parse_error_template_xml(doc) - return response - - response = {} - response['request_status'] = request_status - - dir_enabled = doc.xpath('//xml/header/dirEnabled[1]') - if len(dir_enabled) == 1: - parse_and_set_boolean_xml(dir_enabled[0], response, 'dir_enabled') - - dir_updated = doc.xpath('//xml/header/dirUpdated[1]') - if len(dir_updated) == 1: - parse_and_set_boolean_xml(dir_updated[0], response, 'dir_updated') - - error_string = doc.xpath('//xml/header/error[1]') - if len(error_string) == 1: - error_string = etree.tostring(error_string[0], method='text', - encoding=unicode).strip() - response['error_string'] = error_string - - unrevoked = doc.xpath('//xml/header/unrevoked[1]') - if len(unrevoked) == 1: - unrevoked = etree.tostring(unrevoked[0], method='text', - encoding=unicode).strip() - response['unrevoked'] = unrevoked - - update_crl = doc.xpath('//xml/header/updateCRL[1]') - if len(update_crl) == 1: - parse_and_set_boolean_xml(update_crl[0], response, 'update_crl') - - update_crl_success = doc.xpath('//xml/header/updateCRLSuccess[1]') - if len(update_crl_success) == 1: - parse_and_set_boolean_xml(update_crl_success[0], response, 'update_crl_success') - - update_crl_error = doc.xpath('//xml/header/updateCRLError[1]') - if len(update_crl_error) == 1: - update_crl_error = etree.tostring(update_crl_error[0], method='text', - encoding=unicode).strip() - response['update_crl_error'] = update_crl_error - - publish_crl_success = doc.xpath('//xml/header/publishCRLSuccess[1]') - if len(publish_crl_success) == 1: - parse_and_set_boolean_xml(publish_crl_success[0], response, 'publish_crl_success') - - publish_crl_error = doc.xpath('//xml/header/publishCRLError[1]') - if len(publish_crl_error) == 1: - publish_crl_error = etree.tostring(publish_crl_error[0], method='text', - encoding=unicode).strip() - response['publish_crl_error'] = publish_crl_error - - crl_update_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateStatus')][1]") - if len(crl_update_status) == 1: - parse_and_set_boolean_xml(crl_update_status[0], response, 'crl_update_status') - - crl_update_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateError')][1]") - if len(crl_update_error) == 1: - crl_update_error = etree.tostring(crl_update_error[0], method='text', - encoding=unicode).strip() - response['crl_update_error'] = crl_update_error - - crl_publish_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishStatus')][1]") - if len(crl_publish_status) == 1: - parse_and_set_boolean_xml(crl_publish_status[0], response, 'crl_publish_status') - - crl_publish_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishError')][1]") - if len(crl_publish_error) == 1: - crl_publish_error = etree.tostring(crl_publish_error[0], method='text', - encoding=unicode).strip() - response['crl_publish_error'] = crl_publish_error - - serial_number = doc.xpath('//xml/header/serialNumber[1]') - if len(serial_number) == 1: - serial_number = int(serial_number[0].text, 16) # parse as hex - response['serial_number'] = serial_number - response['serial_number_hex'] = u'0x%X' % serial_number - - return response - - def parse_updateCRL_xml(doc): ''' :param doc: The root node of the xml document to parse @@ -1367,15 +766,16 @@ class ra(rabase.rabase, RestClient): +===================+===============+===============+ |serial_number |unicode [1]_ | | +-------------------+---------------+---------------+ - |request_id |unicode | | + |request_id |unicode [1]_ | | +-------------------+---------------+---------------+ |cert_request_status|unicode [2]_ | | +-------------------+---------------+---------------+ - .. [1] Passed through XMLRPC as decimal string. Can convert to - optimal integer type (int or long) via int(serial_number) + .. [1] The certID and requestId values are returned in + JSON as hex regardless of what the request contains. + They are converted to decimal in the return value. - .. [2] cert_request_status may be one of: + .. [2] cert_request_status, requestStatus, may be one of: - "begin" - "pending" @@ -1385,41 +785,72 @@ class ra(rabase.rabase, RestClient): - "rejected" - "complete" + The REST API responds with JSON in the form of: + + { + "requestID": "0x3", + "requestType": "enrollment", + "requestStatus": "complete", + "requestURL": "https://ipa.example.test:8443/ca/rest/certrequests/3", + "certId": "0x3", + "certURL": "https://ipa.example.test:8443/ca/rest/certs/3", + "certRequestType": "pkcs10", + "operationResult": "success", + "requestId": "0x3" + } """ logger.debug('%s.check_request_status()', type(self).__name__) # Call CMS - http_status, _http_headers, http_body = ( - self._request('/ca/ee/ca/checkRequest', - self.env.ca_port, - requestId=request_id, - xml='true') - ) + path = 'certrequests/{}'.format(request_id) + try: + http_status, _http_headers, http_body = self._ssldo( + 'GET', path, use_session=False, + headers={ + 'Accept': 'application/json', + }, + ) + except errors.HTTPRequestError as e: + self.raise_certificate_operation_error( + 'check_request_status', + err_msg=e.msg, + detail=e.status # pylint: disable=no-member + ) # Parse and handle errors if http_status != 200: + # Note: this is a bit of an API change in that the error + # returned contains the hex value of the certificate + # but it's embedded in the 404. I doubt anything relies + # on it. self.raise_certificate_operation_error('check_request_status', detail=http_status) - parse_result = self.get_parse_result_xml(http_body, parse_check_request_result_xml) - request_status = parse_result['request_status'] - if request_status != CMS_STATUS_SUCCESS: - self.raise_certificate_operation_error('check_request_status', - cms_request_status_to_string(request_status), - parse_result.get('error_string')) + try: + parse_result = json.loads(ipautil.decode_json(http_body)) + except ValueError: + logger.debug("Response from CA was not valid JSON: %s", e) + raise errors.RemoteRetrieveError( + reason=_("Response from CA was not valid JSON") + ) + operation_result = parse_result['operationResult'] + if operation_result != "success": + self.raise_certificate_operation_error( + 'check_request_status', + cms_request_status_to_string(operation_result), + parse_result.get('errorMessage')) # Return command result cmd_result = {} - if 'serial_numbers' in parse_result and len(parse_result['serial_numbers']) > 0: - # see module documentation concerning serial numbers and XMLRPC - cmd_result['serial_number'] = unicode(parse_result['serial_numbers'][0]) + if 'certId' in parse_result: + cmd_result['serial_number'] = int(parse_result['certId'], 16) - if 'request_id' in parse_result: - cmd_result['request_id'] = parse_result['request_id'] + if 'requestID' in parse_result: + cmd_result['request_id'] = int(parse_result['requestID'], 16) - if 'cert_request_status' in parse_result: - cmd_result['cert_request_status'] = parse_result['cert_request_status'] + if 'requestStatus' in parse_result: + cmd_result['cert_request_status'] = parse_result['requestStatus'] return cmd_result @@ -1447,7 +878,7 @@ class ra(rabase.rabase, RestClient): .. [1] Base64 encoded - .. [2] Passed through XMLRPC as decimal string. Can convert to + .. [2] Passed through RPC as decimal string. Can convert to optimal integer type (int or long) via int(serial_number) .. [3] revocation reason may be one of: @@ -1486,8 +917,10 @@ class ra(rabase.rabase, RestClient): try: resp = json.loads(ipautil.decode_json(http_body)) except ValueError: + logger.debug("Response from CA was not valid JSON: %s", e) raise errors.RemoteRetrieveError( - reason=_("Response from CA was not valid JSON")) + reason=_("Response from CA was not valid JSON") + ) # Return command result cmd_result = {} @@ -1537,24 +970,45 @@ class ra(rabase.rabase, RestClient): logger.debug('%s.request_certificate()', type(self).__name__) # Call CMS - template = u''' - - {profile} - - certReqInputImpl - - {req_type} - - - {req} - - - ''' + template = ''' + {{ + "ProfileID" : "{profile}", + "Renewal" : false, + "RemoteHost" : "", + "RemoteAddress" : "", + "Input" : [ {{ + "id" : "i1", + "ClassID" : "certReqInputImpl", + "Name" : "Certificate Request Input", + "ConfigAttribute" : [ ], + "Attribute" : [ {{ + "name" : "cert_request_type", + "Value" : "{req_type}", + "Descriptor" : {{ + "Syntax" : "cert_request_type", + "Description" : "Certificate Request Type" + }} + }}, {{ + "name" : "cert_request", + "Value" : "{req}", + "Descriptor" : {{ + "Syntax" : "cert_request", + "Description" : "Certificate Request" + }} + }} ] + }} ], + "Output" : [ ], + "Attributes" : {{ + "Attribute" : [ ] + }} + }} + ''' data = template.format( profile=profile_id, req_type=request_type, req=csr, ) + data = data.replace('\n', '') path = 'certrequests' if ca_id: @@ -1563,7 +1017,7 @@ class ra(rabase.rabase, RestClient): _http_status, _http_headers, http_body = self._ssldo( 'POST', path, headers={ - 'Content-Type': 'application/xml', + 'Content-Type': 'application/json', 'Accept': 'application/json', }, body=data, @@ -1572,8 +1026,11 @@ class ra(rabase.rabase, RestClient): try: resp_obj = json.loads(ipautil.decode_json(http_body)) - except ValueError: - raise errors.RemoteRetrieveError(reason=_("Response from CA was not valid JSON")) + except ValueError as e: + logger.debug("Response from CA was not valid JSON: %s", e) + raise errors.RemoteRetrieveError( + reason=_("Response from CA was not valid JSON") + ) # Return command result cmd_result = {} @@ -1606,10 +1063,11 @@ class ra(rabase.rabase, RestClient): :param serial_number: Certificate serial number. Must be a string value because serial numbers may be of any magnitude and XMLRPC cannot handle integers larger than 64-bit. - The string value should be decimal, but may optionally - be prefixed with a hex radix prefix if the integral value - is represented as hexadecimal. If no radix prefix is - supplied the string will be interpreted as decimal. + The string value should be decimal, but may + optionally be prefixed with a hex radix prefix + if the integral value is represented as + hexadecimal. If no radix prefix is supplied + the string will be interpreted as decimal. :param revocation_reason: Integer code of revocation reason. Revoke a certificate. @@ -1623,54 +1081,106 @@ class ra(rabase.rabase, RestClient): |revoked |bool | | +---------------+---------------+---------------+ + The REST API responds with JSON in the form of: + + { + "requestID": "0x17", + "requestType": "revocation", + "requestStatus": "complete", + "requestURL": "https://ipa.example.test:8443/ca/rest/certrequests/23", + "certId": "0x12", + "certURL": "https://ipa.example.test:8443/ca/rest/certs/18", + "operationResult": "success", + "requestId": "0x17" + } + + requestID appears to be deprecated in favor of requestId. + + The Ids are in hex. IPA has traditionally returned these as + decimal. The REST API raises exceptions using hex which + will be a departure from previous behavior but unless we + scrape it out of the message there isn't much we can do. """ + reasons = ["Unspecified", + "Key_Compromise", + "CA_Compromise", + "Affiliation_Changed", + "Superseded", + "Cessation_of_Operation", + "Certificate_Hold", + "NOTUSED", # value 7 is not used + "Remove_from_CRL", + "Privilege_Withdrawn", + "AA_Compromise"] + logger.debug('%s.revoke_certificate()', type(self).__name__) if type(revocation_reason) is not int: raise TypeError(TYPE_ERROR % ('revocation_reason', int, revocation_reason, type(revocation_reason))) + if revocation_reason == 7: + self.raise_certificate_operation_error( + 'revoke_certificate', + detail='7 is not a valid revocation reason' + ) + # Convert serial number to integral type from string to properly handle - # radix issues. Note: the int object constructor will properly handle large - # magnitude integral values by returning a Python long type when necessary. + # radix issues. Note: the int object constructor will properly handle + # large magnitude integral values by returning a Python long type + # when necessary. serial_number = int(serial_number, 0) - # Call CMS - http_status, _http_headers, http_body = \ - self._sslget('/ca/agent/ca/doRevoke', - self.env.ca_agent_port, - op='revoke', - revocationReason=revocation_reason, - revokeAll='(certRecordId=%s)' % str(serial_number), - totalRecordCount=1, - xml='true') + path = 'agent/certs/{}/revoke'.format(serial_number) + data = '{{"reason":"{}"}}'.format(reasons[revocation_reason]) - # Parse and handle errors + http_status, _http_headers, http_body = self._ssldo( + 'POST', path, + headers={ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body=data, + use_session=False, + ) if http_status != 200: self.raise_certificate_operation_error('revoke_certificate', detail=http_status) - parse_result = self.get_parse_result_xml(http_body, parse_revoke_cert_xml) - request_status = parse_result['request_status'] - if request_status != CMS_STATUS_SUCCESS: - self.raise_certificate_operation_error('revoke_certificate', - cms_request_status_to_string(request_status), - parse_result.get('error_string')) + try: + response = json.loads(ipautil.decode_json(http_body)) + except ValueError as e: + logger.debug("Response from CA was not valid JSON: %s", e) + raise errors.RemoteRetrieveError( + reason=_("Response from CA was not valid JSON") + ) + + request_status = response['operationResult'] + if request_status != 'success': + self.raise_certificate_operation_error( + 'revoke_certificate', + request_status, + response.get('errorMessage') + ) # Return command result cmd_result = {} - cmd_result['revoked'] = parse_result.get('revoked') == 'yes' + # We can assume the revocation was successful because if it failed + # then REST will return a non-200 or operationalResult will not + # be 'success'. + cmd_result['revoked'] = True return cmd_result def take_certificate_off_hold(self, serial_number): """ :param serial_number: Certificate serial number. Must be a string value - because serial numbers may be of any magnitude and - XMLRPC cannot handle integers larger than 64-bit. - The string value should be decimal, but may optionally - be prefixed with a hex radix prefix if the integral value - is represented as hexadecimal. If no radix prefix is - supplied the string will be interpreted as decimal. + because serial numbers may be of any magnitude + and XMLRPC cannot handle integers larger than + 64-bit. The string value should be decimal, but + may optionally be prefixed with a hex radix + prefix if the integral value is represented as + hexadecimal. If no radix prefix is supplied + the string will be interpreted as decimal. Take revoked certificate off hold. @@ -1680,47 +1190,84 @@ class ra(rabase.rabase, RestClient): +---------------+---------------+---------------+ |result name |result type |comments | +===============+===============+===============+ - |unrevoked |bool | | - +---------------+---------------+---------------+ - |error_string |unicode | | + |requestStatus |unicode | | + |errorMessage |unicode | | +---------------+---------------+---------------+ + + The REST API responds with JSON in the form of: + + { + "requestID":"0x19", + "requestType":"unrevocation", + "requestStatus":"complete", + "requestURL":"https://ipa.example.test:8443/ca/rest/certrequests/25", + "operationResult":"success", + "requestId":"0x19" + } + + Being REST, some errors are returned as HTTP codes. Like + not being authenticated (401) or un-revoking a non-revoked + certificate (404). + + For removing hold, unrevoking a non-revoked certificate will + return errorMessage. + + requestID appears to be deprecated in favor of requestId. + + The Ids are in hex. IPA has traditionally returned these as + decimal. The REST API raises exceptions using hex which + will be a departure from previous behavior but unless we + scrape it out of the message there isn't much we can do. """ logger.debug('%s.take_certificate_off_hold()', type(self).__name__) # Convert serial number to integral type from string to properly handle - # radix issues. Note: the int object constructor will properly handle large - # magnitude integral values by returning a Python long type when necessary. + # radix issues. Note: the int object constructor will properly handle + # large magnitude integral values by returning a Python long type when + # necessary. serial_number = int(serial_number, 0) - # Call CMS - http_status, _http_headers, http_body = ( - self._sslget('/ca/agent/ca/doUnrevoke', - self.env.ca_agent_port, - serialNumber=str(serial_number), - xml='true') + path = 'agent/certs/{}/unrevoke'.format(serial_number) + + http_status, _http_headers, http_body = self._ssldo( + 'POST', path, + headers={ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + use_session=False, ) - - # Parse and handle errors if http_status != 200: - self.raise_certificate_operation_error('take_certificate_off_hold', - detail=http_status) + self.raise_certificate_operation_error( + 'take_certificate_off_hold', + detail=http_status) + try: + response = json.loads(ipautil.decode_json(http_body)) + except ValueError as e: + logger.debug("Response from CA was not valid JSON: %s", e) + raise errors.RemoteRetrieveError( + reason=_("Response from CA was not valid JSON") + ) - parse_result = self.get_parse_result_xml(http_body, parse_unrevoke_cert_xml) - request_status = parse_result['request_status'] - if request_status != CMS_STATUS_SUCCESS: - self.raise_certificate_operation_error('take_certificate_off_hold', - cms_request_status_to_string(request_status), - parse_result.get('error_string')) + request_status = response['operationResult'] + if request_status != 'success': + self.raise_certificate_operation_error( + 'take_certificate_off_hold', + request_status, + response.get('errorMessage')) # Return command result cmd_result = {} - if 'error_string' in parse_result: - cmd_result['error_string'] = parse_result['error_string'] + if 'errorMessage' in response: + cmd_result['error_string'] = response['errorMessage'] - cmd_result['unrevoked'] = parse_result.get('unrevoked') == 'yes' + # We can assume the un-revocation was successful because if it failed + # then REST will return a non-200 or operationalResult will not + # be 'success'. + cmd_result['unrevoked'] = True return cmd_result @@ -1740,11 +1287,7 @@ class ra(rabase.rabase, RestClient): logger.debug('%s.find()', type(self).__name__) - # Create the root element - page = etree.Element('CertSearchRequest') - - # Make a new document tree - doc = etree.ElementTree(page) + cert_search_request = dict() # This matches the default configuration of the pki tool. booloptions = {'serialNumberRangeInUse': True, @@ -1764,30 +1307,27 @@ class ra(rabase.rabase, RestClient): booloptions['matchExactly'] = True if 'subject' in options: - node = etree.SubElement(page, 'commonName') - node.text = options['subject'] + cert_search_request['commonName'] = options['subject'] booloptions['subjectInUse'] = True if 'issuer' in options: - node = etree.SubElement(page, 'issuerDN') - node.text = options['issuer'] + cert_search_request['issuerDN'] = options['issuer'] if 'revocation_reason' in options: - node = etree.SubElement(page, 'revocationReason') - node.text = unicode(options['revocation_reason']) + cert_search_request['revocationReason'] = unicode( + options['revocation_reason']) booloptions['revocationReasonInUse'] = True if 'min_serial_number' in options: - node = etree.SubElement(page, 'serialFrom') - node.text = unicode(options['min_serial_number']) + cert_search_request['serialFrom'] = unicode( + options['min_serial_number']) if 'max_serial_number' in options: - node = etree.SubElement(page, 'serialTo') - node.text = unicode(options['max_serial_number']) + cert_search_request['serialTo'] = unicode( + options['max_serial_number']) if 'status' in options: - node = etree.SubElement(page, 'status') - node.text = unicode(options['status']) + cert_search_request['status'] = options['status'] # date_types is a tuple that consists of: # 1. attribute name passed from IPA API @@ -1808,17 +1348,14 @@ class ra(rabase.rabase, RestClient): for (attr, dattr, battr) in date_types: if attr in options: epoch = convert_time(options[attr]) - node = etree.SubElement(page, dattr) - node.text = unicode(epoch) + cert_search_request[dattr] = unicode(epoch) booloptions[battr] = True # Add the boolean options to our XML document for opt, value in booloptions.items(): - e = etree.SubElement(page, opt) - e.text = str(value).lower() + cert_search_request[opt] = str(value).lower() - payload = etree.tostring(doc, pretty_print=False, - xml_declaration=True, encoding='UTF-8') + payload = json.dumps(cert_search_request, sort_keys=True) logger.debug('%s.find(): request: %s', type(self).__name__, payload) url = '/ca/rest/certs/search?size=%d' % ( @@ -1833,18 +1370,24 @@ class ra(rabase.rabase, RestClient): method='POST', headers={'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'IPA', - 'Content-Type': 'application/xml', - 'Accept': 'application/xml'}, + 'Content-Type': 'application/json', + 'Accept': 'application/json'}, body=payload ) - parser = etree.XMLParser() if status != 200: + try: + response = json.loads(ipautil.decode_json(data)) + except ValueError as e: + logger.debug("Response from CA was not valid JSON: %s", e) + self.raise_certificate_operation_error( + 'find', + detail='Failed to parse error response') + # Try to parse out the returned error. If this fails then # raise the generic certificate operations error. try: - doc = etree.fromstring(data, parser) - msg = doc.xpath('//PKIException/Message')[0].text + msg = response.get('Message') msg = msg.split(':', 1)[0] except etree.XMLSyntaxError as e: self.raise_certificate_operation_error('find', @@ -1859,42 +1402,44 @@ class ra(rabase.rabase, RestClient): logger.debug('%s.find(): response: %s', type(self).__name__, data) try: - doc = etree.fromstring(data, parser) - except etree.XMLSyntaxError as e: + data = json.loads(data) + except TypeError as e: self.raise_certificate_operation_error('find', - detail=e.msg) + detail=str(e)) # Grab all the certificates - certs = doc.xpath('//CertDataInfo') + certs = data['entries'] results = [] for cert in certs: response_request = {} - response_request['serial_number'] = int(cert.get('id'), 16) # parse as hex - response_request['serial_number_hex'] = u'0x%X' % response_request['serial_number'] + response_request['serial_number'] = int( + cert.get('id'), 16) # parse as hex + response_request["serial_number_hex"] = ( + "0x%X" % response_request["serial_number"] + ) - dn = cert.xpath('SubjectDN') - if len(dn) == 1: - response_request['subject'] = unicode(dn[0].text) + dn = cert.get('SubjectDN') + if dn: + response_request['subject'] = dn - issuer_dn = cert.xpath('IssuerDN') - if len(issuer_dn) == 1: - response_request['issuer'] = unicode(issuer_dn[0].text) + issuer_dn = cert.get('IssuerDN') + if issuer_dn: + response_request['issuer'] = issuer_dn - not_valid_before = cert.xpath('NotValidBefore') - if len(not_valid_before) == 1: + not_valid_before = cert.get('NotValidBefore') + if not_valid_before: response_request['valid_not_before'] = ( - unicode(not_valid_before[0].text)) + not_valid_before) - not_valid_after = cert.xpath('NotValidAfter') - if len(not_valid_after) == 1: - response_request['valid_not_after'] = ( - unicode(not_valid_after[0].text)) + not_valid_after = cert.get('NotValidAfter') + if not_valid_after: + response_request['valid_not_after'] = (not_valid_after) - status = cert.xpath('Status') - if len(status) == 1: - response_request['status'] = unicode(status[0].text) + status = cert.get('Status') + if status: + response_request['status'] = status results.append(response_request) return results @@ -2095,7 +1640,8 @@ class ra_lightweight_ca(RestClient): except Exception as e: logger.debug('%s', e, exc_info=True) raise errors.RemoteRetrieveError( - reason=_("Response from CA was not valid JSON")) + reason=_("Response from CA was not valid JSON") + ) def read_ca(self, ca_id): _status, _resp_headers, resp_body = self._ssldo(