1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 '''
24
25 ==============================================
26 Backend plugin for RA using Dogtag (e.g. CMS)
27 ==============================================
28
29 Overview of interacting with CMS:
30 ---------------------------------
31
32 CMS stands for "Certificate Management System". It has been released under a
33 variety of names, the open source version is called "dogtag".
34
35 CMS consists of a number of servlets which in rough terms can be thought of as
36 RPC commands. A servlet is invoked by making an HTTP request to a specific URL
37 and passing URL arguments. Normally CMS responds with an HTTP reponse consisting
38 of HTML to be rendered by a web browser. This HTTP HTML response has both
39 Javascript SCRIPT components and HTML rendering code. One of the Javascript
40 SCRIPT blocks holds the data for the result. The rest of the response is derived
41 from templates associated with the servlet which may be customized. The
42 templates pull the result data from Javascript variables.
43
44 One way to get the result data is to parse the HTML looking for the Javascript
45 varible initializations. Simple string searchs are not a robust method. First of
46 all one must be sure the string is only found in a Javascript SCRIPT block and
47 not somewhere else in the HTML document. Some of the Javascript variable
48 initializations are rather complex (e.g. lists of structures). It would be hard
49 to correctly parse such complex and diverse Javascript. Existing Javascript
50 parsers are not generally available. Finally, it's important to know the
51 character encoding for strings. There is a somewhat complex set of precident
52 rules for determining the current character encoding from the HTTP header,
53 meta-equiv tags, mime Content-Type and charset attributes on HTML elements. All
54 of this means trying to read the result data from a CMS HTML response is
55 difficult to do robustly.
56
57 However, CMS also supports returning the result data as a XML document
58 (distinct from an XHTML document which would be essentially the same as
59 described above). There are a wide variety of tools to robustly parse
60 XML. Because XML is so well defined things like escapes, character encodings,
61 etc. are automatically handled by the tools.
62
63 Thus we never try to parse Javascript, instead we always ask CMS to return us an
64 XML document by passing the URL argument xml="true". The body of the HTTP
65 response is an XML document rather than HTML with embedded Javascript.
66
67 To parse the XML documents we use the Python lxml package which is a Python
68 binding around the libxml2 implementation. libxml2 is a very fast, standard
69 compliant, feature full XML implementation. libxml2 is the XML library of choice
70 for many projects. One of the features in lxml and libxml2 that is particularly
71 valuable to us is the XPath implementation. We make heavy use of XPath to find
72 data in the XML documents we're parsing.
73
74 Parse Results vs. IPA command results:
75 --------------------------------------
76
77 CMS results can be parsed from either HTML or XML. CMS unfortunately is not
78 consistent with how it names items or how it utilizes data types. IPA has strict
79 rules about data types. Also IPA would like to see a more consistent view CMS
80 data. Therefore we split the task of parsing CMS results out from the IPA
81 command code. The parse functions normalize the result data by using a
82 consistent set of names and data types. The IPA command only deals with the
83 normalized parse results. This also allow us to use different parsers if need be
84 (i.e. if we had to parse Javascript for some reason). The parse functions
85 attempt to parse as must information from the CMS result as is possible. It puts
86 the parse result into a dict whose normalized key/value pairs are easy to
87 access. IPA commands do not need to return all the parsed results, it can pick
88 and choose what it wants to return in the IPA command result from the parse
89 result. It also rest assured the values in the parse result will be the correct
90 data type. Thus the general sequence of steps for an IPA command talking to CMS
91 are:
92
93 #. Receive IPA arguments from IPA command
94 #. Formulate URL with arguments for CMS
95 #. Make request to CMS server
96 #. Extract XML document from HTML body returned by CMS
97 #. Parse XML document using matching parse routine which returns response dict
98 #. Extract relevant items from parse result and insert into command result
99 #. Return command result
100
101 Serial Numbers:
102 ---------------
103
104 Serial numbers are integral values of any magnitude because they are based on
105 ASN.1 integers. CMS uses the Java BigInteger to represent these. Fortunately
106 Python also has support for big integers via the Python long() object. Any
107 BigIntegers we receive from CMS as a string can be parsed into a Python long
108 without loss of information.
109
110 However Python has a neat trick. It normally represents integers via the int
111 object which internally uses the native C long type. If you create an int
112 object by passing the int constructor a string it will check the magnitude of
113 the value. If it would fit in a C long then it returns you an int
114 object. However if the value is too big for a C long type then it returns you
115 a Python long object instead. This is a very nice property because it's much
116 more efficient to use C long types when possible (e.g. Python int), but when
117 necessary you'll get a Python long() object to handle large magnitude
118 values. Python also nicely handles type promotion transparently between int
119 and long objects. For example if you multiply two int objects you may get back
120 a long object if necessary. In general Python int and long objects may be
121 freely mixed without the programmer needing to be aware of which type of
122 intergral object is being operated on.
123
124 The leads to the following rule, always parse a string representing an
125 integral value using the int() constructor even if it might have large
126 magnitude because Python will return either an int or a long automatically. By
127 the same token don't test for type of an object being int exclusively because
128 it could either be an int or a long object.
129
130 Internally we should always being using int or long object to hold integral
131 values. This is because we should be able to compare them correctly, be free
132 from concerns about having the know the radix of the string, perform
133 arithmetic operations, and convert to string representation (with correct
134 radix) when necessary. In other words internally we should never handle
135 integral values as strings.
136
137 However, the XMLRPC transport cannot properly handle a Python long object. The
138 XMLRPC encoder upon seeing a Python long will test to see if the value fits
139 within the range of an 32-bit integer, if so it passes the integer parameter
140 otherwise it raises an Overflow exception. The XMLRPC specification does
141 permit 64-bit integers (e.g. i8) and the Python XMLRPC module could allow long
142 values within the 64-bit range to be passed if it were patched, however this
143 only moves the problem, it does not solve passing big integers through
144 XMLRPC. Thus we must always pass big integers as a strings through the XMLRPC
145 interface. But upon receiving that value from XMLRPC we should convert it back
146 into an int or long object. Recall also that Python will automatically perform
147 a conversion to string if you output the int or long object in a string context.
148
149 Radix Issues:
150 -------------
151
152 CMS uses the following conventions: Serial numbers are always returned as
153 hexadecimal strings without a radix prefix. When CMS takes a serial number as
154 input it accepts the value in either decimal or hexadecimal utilizing the radix
155 prefix (e.g. 0x) to determine how to parse the value.
156
157 IPA has adopted the convention that all integral values in the user interface
158 will use base 10 decimal radix.
159
160 Basic rules on handling these values
161
162 1. Reading a serial number from CMS requires conversion from hexadecimal
163 by converting it into a Python int or long object, use the int constructor:
164
165 >>> serial_number = int(serial_number, 16)
166
167 2. Big integers passed to XMLRPC must be decimal unicode strings
168
169 >>> unicode(serial_number)
170
171 3. Big integers received from XMLRPC must be converted back to int or long
172 objects from the decimal string representation.
173
174 >>> serial_number = int(serial_number)
175
176 Xpath pattern matching on node names:
177 -------------------------------------
178
179 There are many excellent tutorial on how to use xpath to find items in an XML
180 document, as such there is no need to repeat this information here. However,
181 most xpath tutorials make the assumption the node names you're searching for are
182 fixed. For example:
183
184 doc.xpath('//book/chapter[*]/section[2]')
185
186 Selects the second section of every chapter of the book. In this example the
187 node names 'book', 'chapter', 'section' are fixed. But what if the XML document
188 embedded the chapter number in the node name, for example 'chapter1',
189 'chapter2', etc.? (If you're thinking this would be incredibly lame, you're
190 right, but sadly people do things like this). Thus in this case you can't use
191 the node name 'chapter' in the xpath location step because it's not fixed and
192 hence won't match 'chapter1', 'chapter2', etc. The solution to this seems
193 obvious, use some type of pattern matching on the node name. Unfortunately this
194 advanced use of xpath is seldom discussed in tutorials and it's not obvious how
195 to do it. Here are some hints.
196
197 Use the built-in xpath string functions. Most of the examples illustrate the
198 string function being passed the text *contents* of the node via '.' or
199 string(.). However we don't want to pass the contents of the node, instead we
200 want to pass the node name. To do this use the name() function. One way we could
201 solve the chapter problem above is by using a predicate which says if the node
202 name begins with 'chapter' it's a match. Here is how you can do that.
203
204 >>> doc.xpath("//book/*[starts-with(name(), 'chapter')]/section[2]")
205
206 The built-in starts-with() returns true if it's first argument starts with it's
207 second argument. Thus the example above says if the node name of the second
208 location step begins with 'chapter' consider it a match and the search
209 proceeds to the next location step, which in this example is any node named
210 'section'.
211
212 But what if we would like to utilize the power of regular expressions to perform
213 the test against the node name? In this case we can use the EXSLT regular
214 expression extension. EXSLT extensions are accessed by using XML
215 namespaces. The regular expression name space identifier is 're:' In lxml we
216 need to pass a set of namespaces to XPath object constructor in order to allow
217 it to bind to those namespaces during it's evaluation. Then we just use the
218 EXSLT regular expression match() function on the node name. Here is how this is
219 done:
220
221 >>> regexpNS = "http://exslt.org/regular-expressions"
222 >>> find = etree.XPath("//book/*[re:match(name(), '^chapter(_\d+)$')]/section[2]",
223 ... namespaces={'re':regexpNS}
224 >>> find(doc)
225
226 What is happening here is that etree.XPath() has returned us an evaluator
227 function which we bind to the name 'find'. We've passed it a set of namespaces
228 as a dict via the 'namespaces' keyword parameter of etree.XPath(). The predicate
229 for the second location step uses the 're:' namespace to find the function name
230 'match'. The re:match() takes a string to search as it's first argument and a
231 regular expression pattern as it's second argument. In this example the string
232 to seach is the node name of the location step because we called the built-in
233 node() function of XPath. The regular expression pattern we've passed says it's
234 a match if the string begins with 'chapter' is followed by any number of
235 digits and nothing else follows.
236
237 '''
238
239 from lxml import etree
240 import datetime
241
242
243
244 CMS_SUCCESS = 0
245 CMS_FAILURE = 1
246 CMS_AUTH_FAILURE = 2
247
248
249
250 CMS_STATUS_UNAUTHORIZED = 1
251 CMS_STATUS_SUCCESS = 2
252 CMS_STATUS_PENDING = 3
253 CMS_STATUS_SVC_PENDING = 4
254 CMS_STATUS_REJECTED = 5
255 CMS_STATUS_ERROR = 6
256 CMS_STATUS_EXCEPTION = 7
257
259 '''
260 :param request_status: The integral request status value
261 :return: String name of request status
262 '''
263 return {
264 1 : 'UNAUTHORIZED',
265 2 : 'SUCCESS',
266 3 : 'PENDING',
267 4 : 'SVC_PENDING',
268 5 : 'REJECTED',
269 6 : 'ERROR',
270 7 : 'EXCEPTION',
271 }.get(request_status, "unknown(%d)" % request_status)
272
274 '''
275 :param error_code: The integral error code value
276 :return: String name of the error code
277 '''
278 return {
279 0 : 'SUCCESS',
280 1 : 'FAILURE',
281 2 : 'AUTH_FAILURE',
282 }.get(error_code, "unknown(%d)" % error_code)
283
285 '''
286 :param node: xml node object containing value to parse for boolean result
287 :param response: response dict to set boolean result in
288 :param response_name: name of the respone value to set
289 :except ValueError:
290
291 Read the value out of a xml text node and interpret it as a boolean value.
292 The text values are stripped of whitespace and converted to lower case
293 prior to interpretation.
294
295 If the value is recognized the response dict is updated using the
296 request_name as the key and the value is set to the bool value of either
297 True or False depending on the interpretation of the text value. If the text
298 value is not recognized a ValueError exception is thrown.
299
300 Text values which result in True:
301
302 - true
303 - yes
304 - on
305
306 Text values which result in False:
307
308 - false
309 - no
310 - off
311 '''
312 value = node.text.strip().lower()
313 if value == 'true' or value == 'yes':
314 value = True
315 elif value == 'false' or value == 'no':
316 value = False
317 else:
318 raise ValueError('expected true|false|yes|no|on|off for "%s", but got "%s"' % \
319 (response_name, value))
320 response[response_name] = value
321
323 '''
324 :param doc: The root node of the xml document to parse
325 :returns: error code as an integer or None if not found
326
327 Returns the error code when the servlet replied with
328 CMSServlet.outputError()
329
330 The possible error code values are:
331
332 - CMS_SUCCESS = 0
333 - CMS_FAILURE = 1
334 - CMS_AUTH_FAILURE = 2
335
336 However, profileSubmit sometimes also returns these values:
337
338 - EXCEPTION = 1
339 - DEFERRED = 2
340 - REJECTED = 3
341
342 '''
343
344 error_code = doc.xpath('//XMLResponse/Status[1]')
345 if len(error_code) == 1:
346 error_code = int(error_code[0].text)
347 else:
348
349
350 error_string = doc.xpath('//XMLResponse/Error[1]')
351 if len(error_string) == 1:
352 error_code = CMS_FAILURE
353 else:
354
355 error_code = CMS_SUCCESS
356
357 return error_code
358
360 '''
361 :param doc: The root node of the xml document to parse
362 :returns: request status as an integer
363
364 Returns the request status from a CMS operation. May be one of:
365
366 - CMS_STATUS_UNAUTHORIZED = 1
367 - CMS_STATUS_SUCCESS = 2
368 - CMS_STATUS_PENDING = 3
369 - CMS_STATUS_SVC_PENDING = 4
370 - CMS_STATUS_REJECTED = 5
371 - CMS_STATUS_ERROR = 6
372 - CMS_STATUS_EXCEPTION = 7
373
374 CMS will often fail to return requestStatus when the status is
375 SUCCESS. Therefore if we fail to find a requestStatus field we default the
376 result to CMS_STATUS_SUCCESS.
377 '''
378
379 request_status = doc.xpath('//xml/fixed/requestStatus[1]')
380 if len(request_status) == 1:
381 request_status = int(request_status[0].text)
382 else:
383
384 request_status = CMS_STATUS_SUCCESS
385
386
387
388
389
390 error_detail = doc.xpath('//xml/fixed/errorDetails[1]')
391 if len(error_detail) == 1 and len(error_detail[0].text.strip()) > 0:
392
393
394 if not (request_status in (CMS_STATUS_ERROR, CMS_STATUS_EXCEPTION)):
395 request_status = CMS_STATUS_ERROR
396
397 return request_status
398
399
401 '''
402 :param doc: The root node of the xml document to parse
403 :returns: result dict
404
405 CMS currently returns errors via XML as either a "template" document
406 (generated by CMSServlet.outputXML() or a "response" document (generated by
407 CMSServlet.outputError()).
408
409 This routine is used to parse a "template" style error or exception
410 document.
411
412 This routine should be use when the CMS requestStatus is ERROR or
413 EXCEPTION. It is capable of parsing both. A CMS ERROR occurs when a known
414 anticipated error condition occurs (e.g. asking for an item which does not
415 exist). A CMS EXCEPTION occurs when an exception is thrown in the CMS server
416 and it's not caught and converted into an ERROR. Think of EXCEPTIONS as the
417 "catch all" error situation.
418
419 ERROR's and EXCEPTIONS's both have error message strings associated with
420 them. For an ERROR it's errorDetails, for an EXCEPTION it's
421 unexpectedError. In addition an EXCEPTION may include an array of additional
422 error strings in it's errorDescription field.
423
424 After parsing the results are returned in a result dict. The following
425 table illustrates the mapping from the CMS data item to what may be found in
426 the result dict. If a CMS data item is absent it will also be absent in the
427 result dict.
428
429 +----------------+---------------+------------------+---------------+
430 |cms name |cms type |result name |result type |
431 +================+===============+==================+===============+
432 |requestStatus |int |request_status |int |
433 +----------------+---------------+------------------+---------------+
434 |errorDetails |string |error_string [1]_ |unicode |
435 +----------------+---------------+------------------+---------------+
436 |unexpectedError |string |error_string [1]_ |unicode |
437 +----------------+---------------+------------------+---------------+
438 |errorDescription|[string] |error_descriptions|[unicode] |
439 +----------------+---------------+------------------+---------------+
440 |authority |string |authority |unicode |
441 +----------------+---------------+------------------+---------------+
442
443 .. [1] errorDetails is the error message string when the requestStatus
444 is ERROR. unexpectedError is the error message string when
445 the requestStatus is EXCEPTION. This routine recognizes both
446 ERROR's and EXCEPTION's and depending on which is found folds
447 the error message into the error_string result value.
448 '''
449
450 response = {}
451 response['request_status'] = CMS_STATUS_ERROR
452
453
454 request_status = doc.xpath('//xml/fixed/requestStatus[1]')
455 if len(request_status) == 1:
456 request_status = int(request_status[0].text)
457 response['request_status'] = request_status
458
459 error_descriptions = []
460 for description in doc.xpath('//xml/records[*]/record/errorDescription'):
461 error_descriptions.append(etree.tostring(description, method='text',
462 encoding=unicode).strip())
463 if len(error_descriptions) > 0:
464 response['error_descriptions'] = error_descriptions
465
466 authority = doc.xpath('//xml/fixed/authorityName[1]')
467 if len(authority) == 1:
468 authority = etree.tostring(authority[0], method='text',
469 encoding=unicode).strip()
470 response['authority'] = authority
471
472
473 error_detail = doc.xpath('//xml/fixed/errorDetails[1]')
474 if len(error_detail) == 1:
475 error_detail = etree.tostring(error_detail[0], method='text',
476 encoding=unicode).strip()
477 response['error_string'] = error_detail
478
479 unexpected_error = doc.xpath('//xml/fixed/unexpectedError[1]')
480 if len(unexpected_error) == 1:
481 unexpected_error = etree.tostring(unexpected_error[0], method='text',
482 encoding=unicode).strip()
483 response['error_string'] = unexpected_error
484
485 return response
486
487
489 '''
490 :param doc: The root node of the xml document to parse
491 :returns: result dict
492
493 CMS currently returns errors via XML as either a "template" document
494 (generated by CMSServlet.outputXML() or a "response" document (generated by
495 CMSServlet.outputError()).
496
497 This routine is used to parse a "response" style error document.
498
499 +---------------+---------------+---------------+---------------+
500 |cms name |cms type |result name |result type |
501 +===============+===============+===============+===============+
502 |Status |int |error_code |int [1]_ |
503 +---------------+---------------+---------------+---------------+
504 |Error |string |error_string |unicode |
505 +---------------+---------------+---------------+---------------+
506 |RequestID |string |request_id |string |
507 +---------------+---------------+---------------+---------------+
508
509 .. [1] error code may be one of:
510
511 - CMS_SUCCESS = 0
512 - CMS_FAILURE = 1
513 - CMS_AUTH_FAILURE = 2
514
515 However, profileSubmit sometimes also returns these values:
516
517 - EXCEPTION = 1
518 - DEFERRED = 2
519 - REJECTED = 3
520
521 '''
522
523 response = {}
524 response['error_code'] = CMS_FAILURE
525
526 error_code = doc.xpath('//XMLResponse/Status[1]')
527 if len(error_code) == 1:
528 error_code = int(error_code[0].text)
529 response['error_code'] = error_code
530
531 error_string = doc.xpath('//XMLResponse/Error[1]')
532 if len(error_string) == 1:
533 error_string = etree.tostring(error_string[0], method='text',
534 encoding=unicode).strip()
535 response['error_string'] = error_string
536
537 request_id = doc.xpath('//XMLResponse/RequestId[1]')
538 if len(request_id) == 1:
539 request_id = etree.tostring(request_id[0], method='text',
540 encoding=unicode).strip()
541 response['request_id'] = request_id
542
543 return response
544
546 '''
547 :param doc: The root node of the xml document to parse
548 :returns: result dict
549 :except ValueError:
550
551 CMS returns an error code and an array of request records.
552
553 This function returns a response dict with the following format:
554 {'error_code' : int, 'requests' : [{}]}
555
556 The mapping of fields and data types is illustrated in the following table.
557
558 If the error_code is not SUCCESS then the response dict will have the
559 contents described in `parse_error_response_xml`.
560
561 +--------------------+----------------+------------------------+---------------+
562 |cms name |cms type |result name |result type |
563 +====================+================+========================+===============+
564 |Status |int |error_code |int |
565 +--------------------+----------------+------------------------+---------------+
566 |Requests[].Id |string |requests[].request_id |unicode |
567 +--------------------+----------------+------------------------+---------------+
568 |Requests[].SubjectDN|string |requests[].subject |unicode |
569 +--------------------+----------------+------------------------+---------------+
570 |Requests[].serialno |BigInteger |requests[].serial_number|int|long |
571 +--------------------+----------------+------------------------+---------------+
572 |Requests[].b64 |string |requests[].certificate |unicode [1]_ |
573 +--------------------+----------------+------------------------+---------------+
574 |Requests[].pkcs7 |string | | |
575 +--------------------+----------------+------------------------+---------------+
576
577 .. [1] Base64 encoded
578
579 '''
580
581 error_code = get_error_code_xml(doc)
582 if error_code != CMS_SUCCESS:
583 response = parse_error_response_xml(doc)
584 return response
585
586 response = {}
587 response['error_code'] = error_code
588
589 requests = []
590 response['requests'] = requests
591
592 for request in doc.xpath('//XMLResponse/Requests[*]/Request'):
593 response_request = {}
594 requests.append(response_request)
595
596 request_id = request.xpath('Id[1]')
597 if len(request_id) == 1:
598 request_id = etree.tostring(request_id[0], method='text',
599 encoding=unicode).strip()
600 response_request['request_id'] = request_id
601
602 subject_dn = request.xpath('SubjectDN[1]')
603 if len(subject_dn) == 1:
604 subject_dn = etree.tostring(subject_dn[0], method='text',
605 encoding=unicode).strip()
606 response_request['subject'] = subject_dn
607
608 serial_number = request.xpath('serialno[1]')
609 if len(serial_number) == 1:
610 serial_number = int(serial_number[0].text, 16)
611 response_request['serial_number'] = serial_number
612
613 certificate = request.xpath('b64[1]')
614 if len(certificate) == 1:
615 certificate = etree.tostring(certificate[0], method='text',
616 encoding=unicode).strip()
617 response_request['certificate'] = certificate
618
619 return response
620
621
623 '''
624 :param doc: The root node of the xml document to parse
625 :returns: result dict
626 :except ValueError:
627
628 After parsing the results are returned in a result dict. The following
629 table illustrates the mapping from the CMS data item to what may be found in
630 the result dict. If a CMS data item is absent it will also be absent in the
631 result dict.
632
633 If the requestStatus is not SUCCESS then the response dict will have the
634 contents described in `parse_error_template_xml`.
635
636 +-------------------------+---------------+-------------------+-----------------+
637 |cms name |cms type |result name |result type |
638 +=========================+===============+===================+=================+
639 |authority |string |authority |unicode |
640 +-------------------------+---------------+-------------------+-----------------+
641 |requestId |string |request_id |string |
642 +-------------------------+---------------+-------------------+-----------------+
643 |staus |string |cert_request_status|unicode [1]_ |
644 +-------------------------+---------------+-------------------+-----------------+
645 |createdOn |long, timestamp|created_on |datetime.datetime|
646 +-------------------------+---------------+-------------------+-----------------+
647 |updatedOn |long, timestamp|updated_on |datetime.datetime|
648 +-------------------------+---------------+-------------------+-----------------+
649 |requestNotes |string |request_notes |unicode |
650 +-------------------------+---------------+-------------------+-----------------+
651 |pkcs7ChainBase64 |string |pkcs7_chain |unicode [2]_ |
652 +-------------------------+---------------+-------------------+-----------------+
653 |cmcFullEnrollmentResponse|string |full_response |unicode [2]_ |
654 +-------------------------+---------------+-------------------+-----------------+
655 |records[].serialNumber |BigInteger |serial_numbers |[int|long] |
656 +-------------------------+---------------+-------------------+-----------------+
657
658 .. [1] cert_request_status may be one of:
659
660 - "begin"
661 - "pending"
662 - "approved"
663 - "svc_pending"
664 - "canceled"
665 - "rejected"
666 - "complete"
667
668 .. [2] Base64 encoded
669
670 '''
671 request_status = get_request_status_xml(doc)
672
673 if request_status != CMS_STATUS_SUCCESS:
674 response = parse_error_template_xml(doc)
675 return response
676
677 response = {}
678 response['request_status'] = request_status
679
680 cert_request_status = doc.xpath('//xml/header/status[1]')
681 if len(cert_request_status) == 1:
682 cert_request_status = etree.tostring(cert_request_status[0], method='text',
683 encoding=unicode).strip()
684 response['cert_request_status'] = cert_request_status
685
686 request_id = doc.xpath('//xml/header/requestId[1]')
687 if len(request_id) == 1:
688 request_id = etree.tostring(request_id[0], method='text',
689 encoding=unicode).strip()
690 response['request_id'] = request_id
691
692 authority = doc.xpath('//xml/header/authority[1]')
693 if len(authority) == 1:
694 authority = etree.tostring(authority[0], method='text',
695 encoding=unicode).strip()
696 response['authority'] = authority
697
698 updated_on = doc.xpath('//xml/header/updatedOn[1]')
699 if len(updated_on) == 1:
700 updated_on = datetime.datetime.utcfromtimestamp(int(updated_on[0].text))
701 response['updated_on'] = updated_on
702
703 created_on = doc.xpath('//xml/header/createdOn[1]')
704 if len(created_on) == 1:
705 created_on = datetime.datetime.utcfromtimestamp(int(created_on[0].text))
706 response['created_on'] = created_on
707
708 request_notes = doc.xpath('//xml/header/requestNotes[1]')
709 if len(request_notes) == 1:
710 request_notes = etree.tostring(request_notes[0], method='text',
711 encoding=unicode).strip()
712 response['request_notes'] = request_notes
713
714 pkcs7_chain = doc.xpath('//xml/header/pkcs7ChainBase64[1]')
715 if len(pkcs7_chain) == 1:
716 pkcs7_chain = etree.tostring(pkcs7_chain[0], method='text',
717 encoding=unicode).strip()
718 response['pkcs7_chain'] = pkcs7_chain
719
720 full_response = doc.xpath('//xml/header/cmcFullEnrollmentResponse[1]')
721 if len(full_response) == 1:
722 full_response = etree.tostring(full_response[0], method='text',
723 encoding=unicode).strip()
724 response['full_response'] = full_response
725
726 serial_numbers = []
727 response['serial_numbers'] = serial_numbers
728 for serial_number in doc.xpath('//xml/records[*]/record/serialNumber'):
729 serial_number = int(serial_number.text, 16)
730 serial_numbers.append(serial_number)
731
732 return response
733
735 '''
736 :param doc: The root node of the xml document to parse
737 :returns: result dict
738 :except ValueError:
739
740 After parsing the results are returned in a result dict. The following
741 table illustrates the mapping from the CMS data item to what may be found in
742 the result dict. If a CMS data item is absent it will also be absent in the
743 result dict.
744
745 If the requestStatus is not SUCCESS then the response dict will have the
746 contents described in `parse_error_template_xml`.
747
748 +----------------+---------------+-----------------+---------------+
749 |cms name |cms type |result name |result type |
750 +================+===============+=================+===============+
751 |emailCert |Boolean |email_cert |bool |
752 +----------------+---------------+-----------------+---------------+
753 |noCertImport |Boolean |no_cert_import |bool |
754 +----------------+---------------+-----------------+---------------+
755 |revocationReason|int |revocation_reason|int [1]_ |
756 +----------------+---------------+-----------------+---------------+
757 |certPrettyPrint |string |cert_pretty |unicode |
758 +----------------+---------------+-----------------+---------------+
759 |authorityid |string |authority |unicode |
760 +----------------+---------------+-----------------+---------------+
761 |certFingerprint |string |fingerprint |unicode |
762 +----------------+---------------+-----------------+---------------+
763 |certChainBase64 |string |certificate |unicode [2]_ |
764 +----------------+---------------+-----------------+---------------+
765 |serialNumber |string |serial_number |int|long |
766 +----------------+---------------+-----------------+---------------+
767 |pkcs7ChainBase64|string |pkcs7_chain |unicode [2]_ |
768 +----------------+---------------+-----------------+---------------+
769
770 .. [1] revocation reason may be one of:
771
772 - 0 = UNSPECIFIED
773 - 1 = KEY_COMPROMISE
774 - 2 = CA_COMPROMISE
775 - 3 = AFFILIATION_CHANGED
776 - 4 = SUPERSEDED
777 - 5 = CESSATION_OF_OPERATION
778 - 6 = CERTIFICATE_HOLD
779 - 8 = REMOVE_FROM_CRL
780 - 9 = PRIVILEGE_WITHDRAWN
781 - 10 = AA_COMPROMISE
782
783 .. [2] Base64 encoded
784
785 '''
786
787 request_status = get_request_status_xml(doc)
788
789 if request_status != CMS_STATUS_SUCCESS:
790 response = parse_error_template_xml(doc)
791 return response
792
793 response = {}
794 response['request_status'] = request_status
795
796 email_cert = doc.xpath('//xml/header/emailCert[1]')
797 if len(email_cert) == 1:
798 parse_and_set_boolean_xml(email_cert[0], response, 'email_cert')
799
800 no_cert_import = doc.xpath('//xml/header/noCertImport[1]')
801 if len(no_cert_import) == 1:
802 parse_and_set_boolean_xml(no_cert_import[0], response, 'no_cert_import')
803
804 revocation_reason = doc.xpath('//xml/header/revocationReason[1]')
805 if len(revocation_reason) == 1:
806 revocation_reason = int(revocation_reason[0].text)
807 response['revocation_reason'] = revocation_reason
808
809 cert_pretty = doc.xpath('//xml/header/certPrettyPrint[1]')
810 if len(cert_pretty) == 1:
811 cert_pretty = etree.tostring(cert_pretty[0], method='text',
812 encoding=unicode).strip()
813 response['cert_pretty'] = cert_pretty
814
815 authority = doc.xpath('//xml/header/authorityid[1]')
816 if len(authority) == 1:
817 authority = etree.tostring(authority[0], method='text',
818 encoding=unicode).strip()
819 response['authority'] = authority
820
821 fingerprint = doc.xpath('//xml/header/certFingerprint[1]')
822 if len(fingerprint) == 1:
823 fingerprint = etree.tostring(fingerprint[0], method='text',
824 encoding=unicode).strip()
825 response['fingerprint'] = fingerprint
826
827 certificate = doc.xpath('//xml/header/certChainBase64[1]')
828 if len(certificate) == 1:
829 certificate = etree.tostring(certificate[0], method='text',
830 encoding=unicode).strip()
831 response['certificate'] = certificate
832
833 serial_number = doc.xpath('//xml/header/serialNumber[1]')
834 if len(serial_number) == 1:
835 serial_number = int(serial_number[0].text, 16)
836 response['serial_number'] = serial_number
837
838 pkcs7_chain = doc.xpath('//xml/header/pkcs7ChainBase64[1]')
839 if len(pkcs7_chain) == 1:
840 pkcs7_chain = etree.tostring(pkcs7_chain[0], method='text',
841 encoding=unicode).strip()
842 response['pkcs7_chain'] = pkcs7_chain
843
844 return response
845
847 '''
848 :param doc: The root node of the xml document to parse
849 :returns: result dict
850 :except ValueError:
851
852 After parsing the results are returned in a result dict. The following
853 table illustrates the mapping from the CMS data item to what may be found in
854 the result dict. If a CMS data item is absent it will also be absent in the
855 result dict.
856
857 If the requestStatus is not SUCCESS then the response dict will have the
858 contents described in `parse_error_template_xml`.
859
860 +----------------------+----------------+-----------------------+---------------+
861 |cms name |cms type |result name |result type |
862 +======================+================+=======================+===============+
863 |dirEnabled |string [1]_ |dir_enabled |bool |
864 +----------------------+----------------+-----------------------+---------------+
865 |certsUpdated |int |certs_updated |int |
866 +----------------------+----------------+-----------------------+---------------+
867 |certsToUpdate |int |certs_to_update |int |
868 +----------------------+----------------+-----------------------+---------------+
869 |error |string [2]_ |error_string |unicode |
870 +----------------------+----------------+-----------------------+---------------+
871 |revoked |string [3]_ |revoked |unicode |
872 +----------------------+----------------+-----------------------+---------------+
873 |totalRecordCount |int |total_record_count |int |
874 +----------------------+----------------+-----------------------+---------------+
875 |updateCRL |string [1]_ [4]_|update_crl |bool |
876 +----------------------+----------------+-----------------------+---------------+
877 |updateCRLSuccess |string [1]_ [4]_|update_crl_success |bool |
878 +----------------------+----------------+-----------------------+---------------+
879 |updateCRLError |string [4]_ |update_crl_error |unicode |
880 +----------------------+----------------+-----------------------+---------------+
881 |publishCRLSuccess |string [1]_[4]_ |publish_crl_success |bool |
882 +----------------------+----------------+-----------------------+---------------+
883 |publishCRLError |string [4]_ |publish_crl_error |unicode |
884 +----------------------+----------------+-----------------------+---------------+
885 |crlUpdateStatus |string [1]_ [5]_|crl_update_status |bool |
886 +----------------------+----------------+-----------------------+---------------+
887 |crlUpdateError |string [5]_ |crl_update_error |unicode |
888 +----------------------+----------------+-----------------------+---------------+
889 |crlPublishStatus |string [1]_ [5]_|crl_publish_status |bool |
890 +----------------------+----------------+-----------------------+---------------+
891 |crlPublishError |string [5]_ |crl_publish_error |unicode |
892 +----------------------+----------------+-----------------------+---------------+
893 |records[].serialNumber|BigInteger |records[].serial_number|int|long |
894 +----------------------+----------------+-----------------------+---------------+
895 |records[].error |string [2]_ |records[].error_string |unicode |
896 +----------------------+----------------+-----------------------+---------------+
897
898 .. [1] String value is either "yes" or "no"
899 .. [2] Sometimes the error string is empty (null)
900 .. [3] revoked may be one of:
901
902 - "yes"
903 - "no"
904 - "begin"
905 - "pending"
906 - "approved"
907 - "svc_pending"
908 - "canceled"
909 - "rejected"
910 - "complete"
911
912 .. [4] Only sent if CRL update information is available.
913 If sent it's only value is "yes".
914 If sent then the following values may also be sent,
915 otherwise they will be absent:
916
917 - updateCRLSuccess
918 - updateCRLError
919 - publishCRLSuccess
920 - publishCRLError
921
922 .. [5] The cms name varies depending on whether the issuing point is MasterCRL
923 or not. If the issuing point is not the MasterCRL then the cms name
924 will be appended with an underscore and the issuing point name.
925 Thus for example the cms name crlUpdateStatus will be crlUpdateStatus
926 if the issuing point is the MasterCRL. However if the issuing point
927 is "foobar" then crlUpdateStatus will be crlUpdateStatus_foobar.
928 When we return the response dict the key will always be the "base"
929 name without the _issuing_point suffix. Thus crlUpdateStatus_foobar
930 will appear in the response dict under the key 'crl_update_status'
931
932 '''
933
934 request_status = get_request_status_xml(doc)
935
936 if request_status != CMS_STATUS_SUCCESS:
937 response = parse_error_template_xml(doc)
938 return response
939
940 response = {}
941 response['request_status'] = request_status
942
943 records = []
944 response['records'] = records
945
946 dir_enabled = doc.xpath('//xml/header/dirEnabled[1]')
947 if len(dir_enabled) == 1:
948 parse_and_set_boolean_xml(dir_enabled[0], response, 'dir_enabled')
949
950 certs_updated = doc.xpath('//xml/header/certsUpdated[1]')
951 if len(certs_updated) == 1:
952 certs_updated = int(certs_updated[0].text)
953 response['certs_updated'] = certs_updated
954
955 certs_to_update = doc.xpath('//xml/header/certsToUpdate[1]')
956 if len(certs_to_update) == 1:
957 certs_to_update = int(certs_to_update[0].text)
958 response['certs_to_update'] = certs_to_update
959
960 error_string = doc.xpath('//xml/header/error[1]')
961 if len(error_string) == 1:
962 error_string = etree.tostring(error_string[0], method='text',
963 encoding=unicode).strip()
964 response['error_string'] = error_string
965
966 revoked = doc.xpath('//xml/header/revoked[1]')
967 if len(revoked) == 1:
968 revoked = etree.tostring(revoked[0], method='text',
969 encoding=unicode).strip()
970 response['revoked'] = revoked
971
972 total_record_count = doc.xpath('//xml/header/totalRecordCount[1]')
973 if len(total_record_count) == 1:
974 total_record_count = int(total_record_count[0].text)
975 response['total_record_count'] = total_record_count
976
977 update_crl = doc.xpath('//xml/header/updateCRL[1]')
978 if len(update_crl) == 1:
979 parse_and_set_boolean_xml(update_crl[0], response, 'update_crl')
980
981 update_crl_success = doc.xpath('//xml/header/updateCRLSuccess[1]')
982 if len(update_crl_success) == 1:
983 parse_and_set_boolean_xml(update_crl_success[0], response, 'update_crl_success')
984
985 update_crl_error = doc.xpath('//xml/header/updateCRLError[1]')
986 if len(update_crl_error) == 1:
987 update_crl_error = etree.tostring(update_crl_error[0], method='text',
988 encoding=unicode).strip()
989 response['update_crl_error'] = update_crl_error
990
991 publish_crl_success = doc.xpath('//xml/header/publishCRLSuccess[1]')
992 if len(publish_crl_success) == 1:
993 parse_and_set_boolean_xml(publish_crl_success[0], response, 'publish_crl_success')
994
995 publish_crl_error = doc.xpath('//xml/header/publishCRLError[1]')
996 if len(publish_crl_error) == 1:
997 publish_crl_error = etree.tostring(publish_crl_error[0], method='text',
998 encoding=unicode).strip()
999 response['publish_crl_error'] = publish_crl_error
1000
1001 crl_update_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateStatus')][1]")
1002 if len(crl_update_status) == 1:
1003 parse_and_set_boolean_xml(crl_update_status[0], response, 'crl_update_status')
1004
1005 crl_update_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateError')][1]")
1006 if len(crl_update_error) == 1:
1007 crl_update_error = etree.tostring(crl_update_error[0], method='text',
1008 encoding=unicode).strip()
1009 response['crl_update_error'] = crl_update_error
1010
1011 crl_publish_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishStatus')][1]")
1012 if len(crl_publish_status) == 1:
1013 parse_and_set_boolean_xml(crl_publish_status[0], response, 'crl_publish_status')
1014
1015 crl_publish_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishError')][1]")
1016 if len(crl_publish_error) == 1:
1017 crl_publish_error = etree.tostring(crl_publish_error[0], method='text',
1018 encoding=unicode).strip()
1019 response['crl_publish_error'] = crl_publish_error
1020
1021 for record in doc.xpath('//xml/records[*]/record'):
1022 response_record = {}
1023 records.append(response_record)
1024
1025 serial_number = record.xpath('serialNumber[1]')
1026 if len(serial_number) == 1:
1027 serial_number = int(serial_number[0].text, 16)
1028 response_record['serial_number'] = serial_number
1029
1030 error_string = record.xpath('error[1]')
1031 if len(error_string) == 1:
1032 error_string = etree.tostring(error_string[0], method='text',
1033 encoding=unicode).strip()
1034 response_record['error_string'] = error_string
1035
1036 return response
1037
1039 '''
1040 :param doc: The root node of the xml document to parse
1041 :returns: result dict
1042 :except ValueError:
1043
1044 After parsing the results are returned in a result dict. The following
1045 table illustrates the mapping from the CMS data item to what may be found in
1046 the result dict. If a CMS data item is absent it will also be absent in the
1047 result dict.
1048
1049 If the requestStatus is not SUCCESS then the response dict will have the
1050 contents described in `parse_error_template_xml`.
1051
1052 +----------------------+----------------+-----------------------+---------------+
1053 |cms name |cms type |result name |result type |
1054 +======================+================+=======================+===============+
1055 |dirEnabled |string [1]_ |dir_enabled |bool |
1056 +----------------------+----------------+-----------------------+---------------+
1057 |dirUpdated |string [1]_ |dir_updated |bool |
1058 +----------------------+----------------+-----------------------+---------------+
1059 |error |string |error_string |unicode |
1060 +----------------------+----------------+-----------------------+---------------+
1061 |unrevoked |string [3]_ |unrevoked |unicode |
1062 +----------------------+----------------+-----------------------+---------------+
1063 |updateCRL |string [1]_ [4]_|update_crl |bool |
1064 +----------------------+----------------+-----------------------+---------------+
1065 |updateCRLSuccess |string [1]_ [4]_|update_crl_success |bool |
1066 +----------------------+----------------+-----------------------+---------------+
1067 |updateCRLError |string [4]_ |update_crl_error |unicode |
1068 +----------------------+----------------+-----------------------+---------------+
1069 |publishCRLSuccess |string [1]_ [4]_|publish_crl_success |bool |
1070 +----------------------+----------------+-----------------------+---------------+
1071 |publishCRLError |string [4]_ |publish_crl_error |unicode |
1072 +----------------------+----------------+-----------------------+---------------+
1073 |crlUpdateStatus |string [1]_ [5]_|crl_update_status |bool |
1074 +----------------------+----------------+-----------------------+---------------+
1075 |crlUpdateError |string [5]_ |crl_update_error |unicode |
1076 +----------------------+----------------+-----------------------+---------------+
1077 |crlPublishStatus |string [1]_ [5]_|crl_publish_status |bool |
1078 +----------------------+----------------+-----------------------+---------------+
1079 |crlPublishError |string [5]_ |crl_publish_error |unicode |
1080 +----------------------+----------------+-----------------------+---------------+
1081 |serialNumber |BigInteger |serial_number |int|long |
1082 +----------------------+----------------+-----------------------+---------------+
1083
1084 .. [1] String value is either "yes" or "no"
1085 .. [3] unrevoked may be one of:
1086
1087 - "yes"
1088 - "no"
1089 - "pending"
1090
1091 .. [4] Only sent if CRL update information is available.
1092 If sent it's only value is "yes".
1093 If sent then the following values may also be sent,
1094 otherwise they will be absent:
1095
1096 - updateCRLSuccess
1097 - updateCRLError
1098 - publishCRLSuccess
1099 - publishCRLError
1100
1101 .. [5] The cms name varies depending on whether the issuing point is MasterCRL
1102 or not. If the issuing point is not the MasterCRL then the cms name
1103 will be appended with an underscore and the issuing point name.
1104 Thus for example the cms name crlUpdateStatus will be crlUpdateStatus
1105 if the issuing point is the MasterCRL. However if the issuing point
1106 is "foobar" then crlUpdateStatus will be crlUpdateStatus_foobar.
1107 When we return the response dict the key will always be the "base"
1108 name without the _issuing_point suffix. Thus crlUpdateStatus_foobar
1109 will appear in the response dict under the key 'crl_update_status'
1110
1111 '''
1112
1113 request_status = get_request_status_xml(doc)
1114
1115 if request_status != CMS_STATUS_SUCCESS:
1116 response = parse_error_template_xml(doc)
1117 return response
1118
1119 response = {}
1120 response['request_status'] = request_status
1121
1122 dir_enabled = doc.xpath('//xml/header/dirEnabled[1]')
1123 if len(dir_enabled) == 1:
1124 parse_and_set_boolean_xml(dir_enabled[0], response, 'dir_enabled')
1125
1126 dir_updated = doc.xpath('//xml/header/dirUpdated[1]')
1127 if len(dir_updated) == 1:
1128 parse_and_set_boolean_xml(dir_updated[0], response, 'dir_updated')
1129
1130 error_string = doc.xpath('//xml/header/error[1]')
1131 if len(error_string) == 1:
1132 error_string = etree.tostring(error_string[0], method='text',
1133 encoding=unicode).strip()
1134 response['error_string'] = error_string
1135
1136 unrevoked = doc.xpath('//xml/header/unrevoked[1]')
1137 if len(unrevoked) == 1:
1138 unrevoked = etree.tostring(unrevoked[0], method='text',
1139 encoding=unicode).strip()
1140 response['unrevoked'] = unrevoked
1141
1142 update_crl = doc.xpath('//xml/header/updateCRL[1]')
1143 if len(update_crl) == 1:
1144 parse_and_set_boolean_xml(update_crl[0], response, 'update_crl')
1145
1146 update_crl_success = doc.xpath('//xml/header/updateCRLSuccess[1]')
1147 if len(update_crl_success) == 1:
1148 parse_and_set_boolean_xml(update_crl_success[0], response, 'update_crl_success')
1149
1150 update_crl_error = doc.xpath('//xml/header/updateCRLError[1]')
1151 if len(update_crl_error) == 1:
1152 update_crl_error = etree.tostring(update_crl_error[0], method='text',
1153 encoding=unicode).strip()
1154 response['update_crl_error'] = update_crl_error
1155
1156 publish_crl_success = doc.xpath('//xml/header/publishCRLSuccess[1]')
1157 if len(publish_crl_success) == 1:
1158 parse_and_set_boolean_xml(publish_crl_success[0], response, 'publish_crl_success')
1159
1160 publish_crl_error = doc.xpath('//xml/header/publishCRLError[1]')
1161 if len(publish_crl_error) == 1:
1162 publish_crl_error = etree.tostring(publish_crl_error[0], method='text',
1163 encoding=unicode).strip()
1164 response['publish_crl_error'] = publish_crl_error
1165
1166 crl_update_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateStatus')][1]")
1167 if len(crl_update_status) == 1:
1168 parse_and_set_boolean_xml(crl_update_status[0], response, 'crl_update_status')
1169
1170 crl_update_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateError')][1]")
1171 if len(crl_update_error) == 1:
1172 crl_update_error = etree.tostring(crl_update_error[0], method='text',
1173 encoding=unicode).strip()
1174 response['crl_update_error'] = crl_update_error
1175
1176 crl_publish_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishStatus')][1]")
1177 if len(crl_publish_status) == 1:
1178 parse_and_set_boolean_xml(crl_publish_status[0], response, 'crl_publish_status')
1179
1180 crl_publish_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishError')][1]")
1181 if len(crl_publish_error) == 1:
1182 crl_publish_error = etree.tostring(crl_publish_error[0], method='text',
1183 encoding=unicode).strip()
1184 response['crl_publish_error'] = crl_publish_error
1185
1186 serial_number = doc.xpath('//xml/header/serialNumber[1]')
1187 if len(serial_number) == 1:
1188 serial_number = int(serial_number[0].text, 16)
1189 response['serial_number'] = serial_number
1190
1191 return response
1192
1193
1194
1195 from ipalib import api, SkipPluginModule
1196 if api.env.ra_plugin != 'dogtag':
1197
1198 raise SkipPluginModule(reason='dogtag not selected as RA plugin')
1199 import os, random, ldap
1200 from ipaserver.plugins import rabase
1201 from ipalib.errors import NetworkError, CertificateOperationError
1202 from ipalib.constants import TYPE_ERROR
1203 from ipapython import dogtag
1204 from ipalib import _
1205
1206 -class ra(rabase.rabase):
1207 """
1208 Request Authority backend plugin.
1209 """
1211 if api.env.in_tree:
1212 self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
1213 self.pwd_file = self.sec_dir + os.sep + '.pwd'
1214 else:
1215 self.sec_dir = "/etc/httpd/alias"
1216 self.pwd_file = "/etc/httpd/alias/pwdfile.txt"
1217 self.noise_file = self.sec_dir + os.sep + '.noise'
1218 self.ipa_key_size = "2048"
1219 self.ipa_certificate_nickname = "ipaCert"
1220 self.ca_certificate_nickname = "caCert"
1221 self.ca_host = None
1222 try:
1223 f = open(self.pwd_file, "r")
1224 self.password = f.readline().strip()
1225 f.close()
1226 except IOError:
1227 self.password = ''
1228 super(ra, self).__init__()
1229
1231 """
1232 :param host: A host which might be a master for a service.
1233 :param service: The service for which the host might be a master.
1234 :return: (true, false)
1235
1236 Check if a specified host is a master for a specified service.
1237 """
1238 base_dn = 'cn=%s,cn=masters,cn=ipa,cn=etc,%s' % (host, api.env.basedn)
1239 filter = '(&(objectClass=ipaConfigObject)(cn=%s)(ipaConfigString=enabledService))' % service
1240 try:
1241 ldap2 = self.api.Backend.ldap2
1242 ent,trunc = ldap2.find_entries(filter=filter, base_dn=base_dn)
1243 if len(ent):
1244 return True
1245 except Exception, e:
1246 pass
1247 return False
1248
1250 """
1251 :param service: The service for which we're looking for a master.
1252 :return: host
1253 as str
1254
1255 Select any host which is a master for a specified service.
1256 """
1257 base_dn = 'cn=masters,cn=ipa,cn=etc,%s' % api.env.basedn
1258 filter = '(&(objectClass=ipaConfigObject)(cn=%s)(ipaConfigString=enabledService))' % service
1259 try:
1260 ldap2 = self.api.Backend.ldap2
1261 ent,trunc = ldap2.find_entries(filter=filter, base_dn=base_dn)
1262 if len(ent):
1263 entry = random.choice(ent)
1264 return ldap.explode_dn(dn=entry[0],notypes=True)[1]
1265 except Exception, e:
1266 pass
1267 return None
1268
1270 """
1271 :return: host
1272 as str
1273
1274 Select our CA host.
1275 """
1276 if self._host_has_service(host=api.env.ca_host):
1277 return api.env.ca_host
1278 if api.env.host != api.env.ca_host:
1279 if self._host_has_service(host=api.env.host):
1280 return api.env.host
1281 host = self._select_any_master()
1282 if host:
1283 return host
1284 else:
1285 return api.env.ca_host
1286
1288 """
1289 :param url: The URL to post to.
1290 :param kw: Keyword arguments to encode into POST body.
1291 :return: (http_status, http_reason_phrase, http_headers, http_body)
1292 as (integer, unicode, dict, str)
1293
1294 Perform an HTTP request.
1295 """
1296 if self.ca_host == None:
1297 self.ca_host = self._select_ca()
1298 return dogtag.http_request(self.ca_host, port, url, **kw)
1299
1300 - def _sslget(self, url, port, **kw):
1301 """
1302 :param url: The URL to post to.
1303 :param kw: Keyword arguments to encode into POST body.
1304 :return: (http_status, http_reason_phrase, http_headers, http_body)
1305 as (integer, unicode, dict, str)
1306
1307 Perform an HTTPS request
1308 """
1309
1310 if self.ca_host == None:
1311 self.ca_host = self._select_ca()
1312 return dogtag.https_request(self.ca_host, port, url, self.sec_dir, self.password, self.ipa_certificate_nickname, **kw)
1313
1315 '''
1316 :param xml_text: The XML text to parse
1317 :param parse_func: The XML parsing function to apply to the parsed DOM tree.
1318 :return: parsed result dict
1319
1320 Utility routine which parses the input text into an XML DOM tree
1321 and then invokes the parsing function on the DOM tree in order
1322 to get the parsing result as a dict of key/value pairs.
1323 '''
1324 parser = etree.XMLParser()
1325 doc = etree.fromstring(xml_text, parser)
1326 result = parse_func(doc)
1327 self.debug("%s() xml_text:\n%s\nparse_result:\n%s" % (parse_func.__name__, xml_text, result))
1328 return result
1329
1331 """
1332 :param request_id: request ID
1333
1334 Check status of a certificate signing request.
1335
1336 The command returns a dict with these possible key/value pairs.
1337 Some key/value pairs may be absent.
1338
1339 +-------------------+---------------+---------------+
1340 |result name |result type |comments |
1341 +===================+===============+===============+
1342 |serial_number |unicode [1]_ | |
1343 +-------------------+---------------+---------------+
1344 |request_id |unicode | |
1345 +-------------------+---------------+---------------+
1346 |cert_request_status|unicode [2]_ | |
1347 +-------------------+---------------+---------------+
1348
1349 .. [1] Passed through XMLRPC as decimal string. Can convert to
1350 optimal integer type (int or long) via int(serial_number)
1351
1352 .. [2] cert_request_status may be one of:
1353
1354 - "begin"
1355 - "pending"
1356 - "approved"
1357 - "svc_pending"
1358 - "canceled"
1359 - "rejected"
1360 - "complete"
1361
1362
1363 """
1364 self.debug('%s.check_request_status()', self.fullname)
1365
1366
1367 http_status, http_reason_phrase, http_headers, http_body = \
1368 self._request('/ca/ee/ca/checkRequest',
1369 self.env.ca_port,
1370 requestId=request_id,
1371 xml='true')
1372
1373
1374 if (http_status != 200):
1375 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
1376 http_reason_phrase)
1377
1378 parse_result = self.get_parse_result_xml(http_body, parse_check_request_result_xml)
1379 request_status = parse_result['request_status']
1380 if request_status != CMS_STATUS_SUCCESS:
1381 raise CertificateOperationError(error='%s (%s)' % \
1382 (cms_request_status_to_string(request_status), parse_result.get('error_string')))
1383
1384
1385 cmd_result = {}
1386 if parse_result.has_key('serial_numbers') and len(parse_result['serial_numbers']) > 0:
1387
1388 cmd_result['serial_number'] = unicode(parse_result['serial_numbers'][0])
1389
1390 if parse_result.has_key('request_id'):
1391 cmd_result['request_id'] = parse_result['request_id']
1392
1393 if parse_result.has_key('cert_request_status'):
1394 cmd_result['cert_request_status'] = parse_result['cert_request_status']
1395
1396 return cmd_result
1397
1399 """
1400 Retrieve an existing certificate.
1401
1402 :param serial_number: Certificate serial number. Must be a string value
1403 because serial numbers may be of any magnitue and
1404 XMLRPC cannot handle integers larger than 64-bit.
1405 The string value should be decimal, but may optionally
1406 be prefixed with a hex radix prefix if the integal value
1407 is represented as hexadecimal. If no radix prefix is
1408 supplied the string will be interpreted as decimal.
1409
1410 The command returns a dict with these possible key/value pairs.
1411 Some key/value pairs may be absent.
1412
1413 +-----------------+---------------+---------------+
1414 |result name |result type |comments |
1415 +=================+===============+===============+
1416 |certificate |unicode [1]_ | |
1417 +-----------------+---------------+---------------+
1418 |serial_number |unicode [2]_ | |
1419 +-----------------+---------------+---------------+
1420 |revocation_reason|int [3]_ | |
1421 +-----------------+---------------+---------------+
1422
1423 .. [1] Base64 encoded
1424
1425 .. [2] Passed through XMLRPC as decimal string. Can convert to
1426 optimal integer type (int or long) via int(serial_number)
1427
1428 .. [3] revocation reason may be one of:
1429
1430 - 0 = UNSPECIFIED
1431 - 1 = KEY_COMPROMISE
1432 - 2 = CA_COMPROMISE
1433 - 3 = AFFILIATION_CHANGED
1434 - 4 = SUPERSEDED
1435 - 5 = CESSATION_OF_OPERATION
1436 - 6 = CERTIFICATE_HOLD
1437 - 8 = REMOVE_FROM_CRL
1438 - 9 = PRIVILEGE_WITHDRAWN
1439 - 10 = AA_COMPROMISE
1440
1441
1442 """
1443 self.debug('%s.get_certificate()', self.fullname)
1444
1445
1446
1447
1448 serial_number = int(serial_number, 0)
1449
1450
1451 http_status, http_reason_phrase, http_headers, http_body = \
1452 self._sslget('/ca/agent/ca/displayBySerial',
1453 self.env.ca_agent_port,
1454 serialNumber=str(serial_number),
1455 xml='true')
1456
1457
1458
1459 if (http_status != 200):
1460 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
1461 http_reason_phrase)
1462
1463 parse_result = self.get_parse_result_xml(http_body, parse_display_cert_xml)
1464 request_status = parse_result['request_status']
1465 if request_status != CMS_STATUS_SUCCESS:
1466 raise CertificateOperationError(error='%s (%s)' % \
1467 (cms_request_status_to_string(request_status), parse_result.get('error_string')))
1468
1469
1470 cmd_result = {}
1471
1472 if parse_result.has_key('certificate'):
1473 cmd_result['certificate'] = parse_result['certificate']
1474
1475 if parse_result.has_key('serial_number'):
1476
1477 cmd_result['serial_number'] = unicode(parse_result['serial_number'])
1478
1479 if parse_result.has_key('revocation_reason'):
1480 cmd_result['revocation_reason'] = parse_result['revocation_reason']
1481
1482 return cmd_result
1483
1484
1486 """
1487 :param csr: The certificate signing request.
1488 :param request_type: The request type (defaults to ``'pkcs10'``).
1489
1490 Submit certificate signing request.
1491
1492 The command returns a dict with these possible key/value pairs.
1493 Some key/value pairs may be absent.
1494
1495 +---------------+---------------+---------------+
1496 |result name |result type |comments |
1497 +===============+===============+===============+
1498 |serial_number |unicode [1]_ | |
1499 +---------------+---------------+---------------+
1500 |certificate |unicode [2]_ | |
1501 +---------------+---------------+---------------+
1502 |request_id |unicode | |
1503 +---------------+---------------+---------------+
1504 |subject |unicode | |
1505 +---------------+---------------+---------------+
1506
1507 .. [1] Passed through XMLRPC as decimal string. Can convert to
1508 optimal integer type (int or long) via int(serial_number)
1509
1510 .. [2] Base64 encoded
1511
1512 """
1513 self.debug('%s.request_certificate()', self.fullname)
1514
1515
1516 http_status, http_reason_phrase, http_headers, http_body = \
1517 self._sslget('/ca/ee/ca/profileSubmitSSLClient',
1518 self.env.ca_ee_port,
1519 profileId='caIPAserviceCert',
1520 cert_request_type=request_type,
1521 cert_request=csr,
1522 xml='true')
1523
1524 if (http_status != 200):
1525 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
1526 http_reason_phrase)
1527
1528 parse_result = self.get_parse_result_xml(http_body, parse_profile_submit_result_xml)
1529
1530 error_code = parse_result['error_code']
1531 if error_code != CMS_SUCCESS:
1532 raise CertificateOperationError(error='%s (%s)' % \
1533 (cms_error_code_to_string(error_code), parse_result.get('error_string')))
1534
1535
1536 cmd_result = {}
1537
1538
1539 if len(parse_result['requests']) < 1:
1540 return cmd_result
1541 request = parse_result['requests'][0]
1542
1543 if request.has_key('serial_number'):
1544
1545 cmd_result['serial_number'] = unicode(request['serial_number'])
1546
1547 if request.has_key('certificate'):
1548 cmd_result['certificate'] = request['certificate']
1549
1550 if request.has_key('request_id'):
1551 cmd_result['request_id'] = request['request_id']
1552
1553 if request.has_key('subject'):
1554 cmd_result['subject'] = request['subject']
1555
1556 return cmd_result
1557
1558
1560 """
1561 :param serial_number: Certificate serial number. Must be a string value
1562 because serial numbers may be of any magnitue and
1563 XMLRPC cannot handle integers larger than 64-bit.
1564 The string value should be decimal, but may optionally
1565 be prefixed with a hex radix prefix if the integal value
1566 is represented as hexadecimal. If no radix prefix is
1567 supplied the string will be interpreted as decimal.
1568 :param revocation_reason: Integer code of revocation reason.
1569
1570 Revoke a certificate.
1571
1572 The command returns a dict with these possible key/value pairs.
1573 Some key/value pairs may be absent.
1574
1575 +---------------+---------------+---------------+
1576 |result name |result type |comments |
1577 +===============+===============+===============+
1578 |revoked |bool | |
1579 +---------------+---------------+---------------+
1580
1581 """
1582 self.debug('%s.revoke_certificate()', self.fullname)
1583 if type(revocation_reason) is not int:
1584 raise TypeError(TYPE_ERROR % ('revocation_reason', int, revocation_reason, type(revocation_reason)))
1585
1586
1587
1588
1589 serial_number = int(serial_number, 0)
1590
1591
1592 http_status, http_reason_phrase, http_headers, http_body = \
1593 self._sslget('/ca/agent/ca/doRevoke',
1594 self.env.ca_agent_port,
1595 op='revoke',
1596 revocationReason=revocation_reason,
1597 revokeAll='(certRecordId=%s)' % str(serial_number),
1598 totalRecordCount=1,
1599 xml='true')
1600
1601
1602 if (http_status != 200):
1603 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
1604 http_reason_phrase)
1605
1606 parse_result = self.get_parse_result_xml(http_body, parse_revoke_cert_xml)
1607 request_status = parse_result['request_status']
1608 if request_status != CMS_STATUS_SUCCESS:
1609 raise CertificateOperationError(error='%s (%s)' % \
1610 (cms_request_status_to_string(request_status), parse_result.get('error_string')))
1611
1612
1613 cmd_result = {}
1614
1615 if parse_result.get('revoked') == 'yes':
1616 cmd_result['revoked'] = True
1617 else:
1618 cmd_result['revoked'] = False
1619
1620 return cmd_result
1621
1623 """
1624 :param serial_number: Certificate serial number. Must be a string value
1625 because serial numbers may be of any magnitue and
1626 XMLRPC cannot handle integers larger than 64-bit.
1627 The string value should be decimal, but may optionally
1628 be prefixed with a hex radix prefix if the integal value
1629 is represented as hexadecimal. If no radix prefix is
1630 supplied the string will be interpreted as decimal.
1631
1632 Take revoked certificate off hold.
1633
1634 The command returns a dict with these possible key/value pairs.
1635 Some key/value pairs may be absent.
1636
1637 +---------------+---------------+---------------+
1638 |result name |result type |comments |
1639 +===============+===============+===============+
1640 |unrevoked |bool | |
1641 +---------------+---------------+---------------+
1642 |error_string |unicode | |
1643 +---------------+---------------+---------------+
1644 """
1645
1646 self.debug('%s.take_certificate_off_hold()', self.fullname)
1647
1648
1649
1650
1651 serial_number = int(serial_number, 0)
1652
1653
1654 http_status, http_reason_phrase, http_headers, http_body = \
1655 self._sslget('/ca/agent/ca/doUnrevoke',
1656 self.env.ca_agent_port,
1657 serialNumber=str(serial_number),
1658 xml='true')
1659
1660
1661 if (http_status != 200):
1662 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
1663 http_reason_phrase)
1664
1665 parse_result = self.get_parse_result_xml(http_body, parse_unrevoke_cert_xml)
1666 request_status = parse_result['request_status']
1667 if request_status != CMS_STATUS_SUCCESS:
1668 raise CertificateOperationError(error='%s (%s)' % \
1669 (cms_request_status_to_string(request_status), parse_result.get('error_string')))
1670
1671
1672 cmd_result = {}
1673
1674 if parse_result.has_key('error_string'):
1675 cmd_result['error_string'] = parse_result['error_string']
1676
1677 if parse_result.get('unrevoked') == 'yes':
1678 cmd_result['unrevoked'] = True
1679 else:
1680 cmd_result['unrevoked'] = False
1681
1682 return cmd_result
1683
1684 api.register(ra)
1685