1 | #!/usr/bin/env python |
---|
2 | """NDG Security Attribute Authority client - client interface classes to the |
---|
3 | Attribute Authority. These have been separated from their |
---|
4 | original location in the SecurityClient since they have the |
---|
5 | unusual place of being required by both client and server |
---|
6 | NDG security packages. For the server side they are required |
---|
7 | as the CredWallet invoked by the Session Manager acts as a |
---|
8 | client to Attribute Authorities when negotiating the required |
---|
9 | Attribute Certificate. |
---|
10 | |
---|
11 | Make requests for Attribute Certificates used for authorisation |
---|
12 | |
---|
13 | NERC Data Grid Project |
---|
14 | """ |
---|
15 | __author__ = "P J Kershaw" |
---|
16 | __date__ = "17/11/06" |
---|
17 | __copyright__ = "(C) 2007 STFC & NERC" |
---|
18 | __contact__ = "P.J.Kershaw@rl.ac.uk" |
---|
19 | __license__ = \ |
---|
20 | """This software may be distributed under the terms of the Q Public |
---|
21 | License, version 1.0 or later.""" |
---|
22 | __contact__ = "P.J.Kershaw@rl.ac.uk" |
---|
23 | __revision__ = "$Id$" |
---|
24 | |
---|
25 | __all__ = [ |
---|
26 | 'AttAuthorityClient', |
---|
27 | 'AttAuthorityClientError', |
---|
28 | 'AttributeRequestDenied', |
---|
29 | 'NoTrustedHosts',] |
---|
30 | |
---|
31 | # Determine https http transport |
---|
32 | import urlparse, httplib |
---|
33 | from ZSI.wstools.Utility import HTTPResponse |
---|
34 | |
---|
35 | from AttAuthority_services import AttAuthorityServiceLocator |
---|
36 | from ndg.security.common.wsSecurity import SignatureHandler |
---|
37 | from ndg.security.common.AttCert import AttCert, AttCertParse |
---|
38 | from ndg.security.common.m2CryptoSSLUtility import HTTPSConnection, \ |
---|
39 | HostCheck |
---|
40 | |
---|
41 | |
---|
42 | #_____________________________________________________________________________ |
---|
43 | class AttAuthorityClientError(Exception): |
---|
44 | """Exception handling for AttributeAuthorityClient class""" |
---|
45 | |
---|
46 | class AttributeRequestDenied(Exception): |
---|
47 | """Raise when a getAttCert call to the AA is denied""" |
---|
48 | |
---|
49 | class NoTrustedHosts(AttAuthorityClientError): |
---|
50 | """Raise from getTrustedHosts if there are no trusted hosts defined in |
---|
51 | the map configuration""" |
---|
52 | |
---|
53 | class NoMatchingRoleInTrustedHosts(AttAuthorityClientError): |
---|
54 | """Raise from getTrustedHosts if there is no mapping to any of the |
---|
55 | trusted hosts for the given input role name""" |
---|
56 | |
---|
57 | #_____________________________________________________________________________ |
---|
58 | class AttAuthorityClient(object): |
---|
59 | """Client interface to Attribute Authority web service |
---|
60 | |
---|
61 | @type excepMap: dict |
---|
62 | @cvar excepMap: map exception strings returned from SOAP fault to client |
---|
63 | Exception class to call""" |
---|
64 | |
---|
65 | excepMap = { |
---|
66 | 'AttAuthorityNoTrustedHosts': NoTrustedHosts, |
---|
67 | 'AttAuthorityNoMatchingRoleInTrustedHosts': NoMatchingRoleInTrustedHosts |
---|
68 | } |
---|
69 | |
---|
70 | #_________________________________________________________________________ |
---|
71 | def __init__(self, |
---|
72 | uri=None, |
---|
73 | tracefile=None, |
---|
74 | sslCACertList=[], |
---|
75 | sslCACertFilePathList=[], |
---|
76 | sslPeerCertCN=None, |
---|
77 | setSignatureHandler=True, |
---|
78 | **signatureHandlerKw): |
---|
79 | """ |
---|
80 | @type uri: string |
---|
81 | @param uri: URI for Attribute Authority WS. Setting it will also |
---|
82 | initialise the Service Proxy |
---|
83 | |
---|
84 | @param tracefile: set to file object such as sys.stderr to give |
---|
85 | extra WS debug information |
---|
86 | |
---|
87 | @type sslCACertList: list |
---|
88 | @param sslCACertList: This keyword is for use with SSL connections |
---|
89 | only. Set a list of one ore more CA certificates. The peer cert. |
---|
90 | must verify against at least one of these otherwise the connection |
---|
91 | is dropped. |
---|
92 | |
---|
93 | @type sslCACertFilePathList: list |
---|
94 | @param sslCACertFilePathList: the same as the above except CA certs |
---|
95 | can be passed as a list of file paths to read from |
---|
96 | |
---|
97 | @type sslPeerCertCN: string |
---|
98 | @param sslPeerCertCN: set an alternate CommonName to match with peer |
---|
99 | cert. This keyword is for use with SSL connections only. |
---|
100 | |
---|
101 | @type setSignatureHandler: bool |
---|
102 | @param setSignatureHandler: flag to determine whether to apply |
---|
103 | WS-Security Signature Handler or not |
---|
104 | |
---|
105 | @type signatureHandlerKw: dict |
---|
106 | @param signatureHandlerKw: keywords to configure signature handler""" |
---|
107 | |
---|
108 | self.__srv = None |
---|
109 | self.__uri = None |
---|
110 | self._transdict = {} |
---|
111 | |
---|
112 | if uri: |
---|
113 | self.__setURI(uri) |
---|
114 | |
---|
115 | if sslPeerCertCN: |
---|
116 | self.__setSSLPeerCertCN(sslPeerCertCN) |
---|
117 | |
---|
118 | if sslCACertList: |
---|
119 | self.__setSSLCACertList(sslCACertList) |
---|
120 | elif sslCACertFilePathList: |
---|
121 | self.__setSSLCACertFilePathList(sslCACertFilePathList) |
---|
122 | |
---|
123 | # WS-Security Signature handler - set only if any of the keywords were |
---|
124 | # set |
---|
125 | if setSignatureHandler: |
---|
126 | self.__signatureHandler = SignatureHandler(**signatureHandlerKw) |
---|
127 | else: |
---|
128 | self.__signatureHandler = None |
---|
129 | |
---|
130 | self.__tracefile = tracefile |
---|
131 | |
---|
132 | # Instantiate Attribute Authority WS proxy |
---|
133 | if self.__uri: |
---|
134 | self.initService() |
---|
135 | |
---|
136 | |
---|
137 | #_________________________________________________________________________ |
---|
138 | def __setURI(self, uri): |
---|
139 | """Set URI for service |
---|
140 | @type uri: string |
---|
141 | @param uri: URI for service to connect to""" |
---|
142 | if not isinstance(uri, basestring): |
---|
143 | raise AttAuthorityClientError, \ |
---|
144 | "Attribute Authority WSDL URI must be a valid string" |
---|
145 | |
---|
146 | self.__uri = uri |
---|
147 | try: |
---|
148 | scheme = urlparse.urlparse(self.__uri)[0] |
---|
149 | except TypeError: |
---|
150 | raise AttributeAuthorityClientError, \ |
---|
151 | "Error parsing transport type from URI" |
---|
152 | |
---|
153 | if scheme == "https": |
---|
154 | self._transport = HTTPSConnection |
---|
155 | else: |
---|
156 | self._transport = None |
---|
157 | |
---|
158 | |
---|
159 | #_________________________________________________________________________ |
---|
160 | def __getURI(self): |
---|
161 | """Get URI for service |
---|
162 | @rtype: string |
---|
163 | @return: uri for service to be invoked""" |
---|
164 | return self.__uri |
---|
165 | |
---|
166 | uri = property(fset=__setURI, fget=__getURI,doc="Attribute Authority URI") |
---|
167 | |
---|
168 | |
---|
169 | #_________________________________________________________________________ |
---|
170 | def __setSSLPeerCertCN(self, cn): |
---|
171 | """For use with HTTPS connections only. Specify the Common |
---|
172 | Name to match with Common Name of the peer certificate. This is not |
---|
173 | needed if the peer cert CN = peer hostname""" |
---|
174 | if self._transport != HTTPSConnection: |
---|
175 | return |
---|
176 | |
---|
177 | if self._transdict.get('postConnectionCheck'): |
---|
178 | self._transdict['postConnectionCheck'].peerCertCN = cn |
---|
179 | else: |
---|
180 | self._transdict['postConnectionCheck'] = HostCheck(peerCertCN=cn) |
---|
181 | |
---|
182 | sslPeerCertCN = property(fset=__setSSLPeerCertCN, |
---|
183 | doc="for https connections, set CN of peer cert if other than peer hostname") |
---|
184 | |
---|
185 | |
---|
186 | #_________________________________________________________________________ |
---|
187 | def __setSSLCACertList(self, caCertList): |
---|
188 | """For use with HTTPS connections only. Specify CA certs to one of |
---|
189 | which the peer cert must verify its signature against""" |
---|
190 | if self._transport != HTTPSConnection: |
---|
191 | return |
---|
192 | |
---|
193 | if self._transdict.get('postConnectionCheck'): |
---|
194 | self._transdict['postConnectionCheck'].caCertList = caCertList |
---|
195 | else: |
---|
196 | self._transdict['postConnectionCheck'] = \ |
---|
197 | HostCheck(caCertList=caCertList) |
---|
198 | |
---|
199 | sslCACertList = property(fset=__setSSLCACertList, |
---|
200 | doc="for https connections, set list of CA certs from which to verify peer cert") |
---|
201 | |
---|
202 | |
---|
203 | #_________________________________________________________________________ |
---|
204 | def __setSSLCACertFilePathList(self, caCertFilePathList): |
---|
205 | """For use with HTTPS connections only. Specify CA certs to one of |
---|
206 | which the peer cert must verify its signature against""" |
---|
207 | if self._transport != HTTPSConnection: |
---|
208 | return |
---|
209 | |
---|
210 | if self._transdict.get('postConnectionCheck'): |
---|
211 | self._transdict['postConnectionCheck'].caCertFilePathList = \ |
---|
212 | caCertFilePathList |
---|
213 | else: |
---|
214 | self._transdict['postConnectionCheck'] = \ |
---|
215 | HostCheck(caCertFilePathList=caCertFilePathList) |
---|
216 | |
---|
217 | sslCACertFilePathList = property(fset=__setSSLCACertFilePathList, |
---|
218 | doc="for https connections, set list of CA cert files from which to verify peer cert") |
---|
219 | |
---|
220 | |
---|
221 | #_________________________________________________________________________ |
---|
222 | def __setSignatureHandler(self, signatureHandler): |
---|
223 | """Set SignatureHandler object property method - set to None to for no |
---|
224 | digital signature and verification""" |
---|
225 | if signatureHandler is not None and \ |
---|
226 | not isinstance(signatureHandler, signatureHandler): |
---|
227 | raise AttributeError, \ |
---|
228 | "Signature Handler must be %s type or None for no message security" % \ |
---|
229 | "ndg.security.common.wsSecurity.SignatureHandler" |
---|
230 | |
---|
231 | self.__signatureHandler = signatureHandler |
---|
232 | |
---|
233 | |
---|
234 | #_________________________________________________________________________ |
---|
235 | def __getSignatureHandler(self): |
---|
236 | "Get SignatureHandler object property method" |
---|
237 | return self.__signatureHandler |
---|
238 | |
---|
239 | signatureHandler = property(fget=__getSignatureHandler, |
---|
240 | fset=__setSignatureHandler, |
---|
241 | doc="SignatureHandler object") |
---|
242 | |
---|
243 | |
---|
244 | #_________________________________________________________________________ |
---|
245 | def initService(self, uri=None): |
---|
246 | """Set the WS proxy for the Attribute Authority |
---|
247 | |
---|
248 | @type uri: string |
---|
249 | @param uri: URI for service to invoke""" |
---|
250 | |
---|
251 | if uri: |
---|
252 | self.__setURI(uri) |
---|
253 | |
---|
254 | # WS-Security Signature handler object is passed to binding |
---|
255 | try: |
---|
256 | locator = AttAuthorityServiceLocator() |
---|
257 | self.__srv = locator.getAttAuthority(self.__uri, |
---|
258 | sig_handler=self.__signatureHandler, |
---|
259 | tracefile=self.__tracefile, |
---|
260 | transport=self._transport, |
---|
261 | transdict=self._transdict) |
---|
262 | except HTTPResponse, e: |
---|
263 | raise AttAuthorityClientError, \ |
---|
264 | "Error initialising service for \"%s\": %s %s" % \ |
---|
265 | (self.__uri, e.status, e.reason) |
---|
266 | |
---|
267 | |
---|
268 | #_________________________________________________________________________ |
---|
269 | def getHostInfo(self): |
---|
270 | """Get host information for the data provider which the |
---|
271 | Attribute Authority represents |
---|
272 | |
---|
273 | @rtype: dict |
---|
274 | @return: dictionary of host information derived from the Attribute |
---|
275 | Authority's map configuration |
---|
276 | """ |
---|
277 | |
---|
278 | try: |
---|
279 | # Convert return tuple into list to enable use of pop() later |
---|
280 | response = list(self.__srv.getHostInfo()) |
---|
281 | except httplib.BadStatusLine, e: |
---|
282 | raise AttAuthorityClientError, "HTTP bad status line: %s" % e |
---|
283 | |
---|
284 | except Exception, e: |
---|
285 | # Try to detect exception type from SOAP fault message |
---|
286 | errMsg = str(e) |
---|
287 | for excep in self.excepMap: |
---|
288 | if excep in errMsg: |
---|
289 | raise self.excepMap[excep] |
---|
290 | |
---|
291 | # Catch all |
---|
292 | raise e |
---|
293 | |
---|
294 | # Unpack response into dict |
---|
295 | hostInfoKw = ['hostname', |
---|
296 | 'aaURI', |
---|
297 | 'aaDN', |
---|
298 | 'loginURI', |
---|
299 | 'loginServerDN', |
---|
300 | 'loginRequestServerDN'] |
---|
301 | hostInfoKw.reverse() |
---|
302 | hostInfo = dict([(k, response.pop()) for k in hostInfoKw]) |
---|
303 | |
---|
304 | return hostInfo |
---|
305 | |
---|
306 | |
---|
307 | #_________________________________________________________________________ |
---|
308 | def getTrustedHostInfo(self, role=None): |
---|
309 | """Get list of trusted hosts for an Attribute Authority |
---|
310 | |
---|
311 | @type role: string |
---|
312 | @param role: get information for trusted hosts that have a mapping to |
---|
313 | this role |
---|
314 | |
---|
315 | @rtype: dict |
---|
316 | @return: dictionary of host information indexed by hostname derived |
---|
317 | from the map configuration""" |
---|
318 | |
---|
319 | try: |
---|
320 | trustedHosts = self.__srv.getTrustedHostInfo(role) |
---|
321 | |
---|
322 | except httplib.BadStatusLine, e: |
---|
323 | raise AttAuthorityClientError, "HTTP bad status line: %s" % e |
---|
324 | |
---|
325 | except Exception, e: |
---|
326 | # Try to detect exception type from SOAP fault message |
---|
327 | errMsg = str(e) |
---|
328 | for excep in self.excepMap: |
---|
329 | if excep in errMsg: |
---|
330 | raise self.excepMap[excep] |
---|
331 | |
---|
332 | # Catch all |
---|
333 | raise e |
---|
334 | |
---|
335 | # Convert into dictionary form as used by AttAuthority class |
---|
336 | trustedHostInfo = {} |
---|
337 | for host in trustedHosts: |
---|
338 | hostname = host.get_element_hostname() |
---|
339 | |
---|
340 | trustedHostInfo[hostname] = \ |
---|
341 | { |
---|
342 | 'aaURI': host.AaURI, |
---|
343 | 'aaDN': host.AaDN, |
---|
344 | 'loginURI': host.LoginURI, |
---|
345 | 'loginServerDN': host.LoginServerDN, |
---|
346 | 'loginRequestServerDN': host.LoginRequestServerDN, |
---|
347 | 'role': host.RoleList |
---|
348 | } |
---|
349 | |
---|
350 | return trustedHostInfo |
---|
351 | |
---|
352 | |
---|
353 | #_________________________________________________________________________ |
---|
354 | def getAllHostsInfo(self): |
---|
355 | """Get list of all hosts for an Attribute Authority i.e. itself and |
---|
356 | all the hosts it trusts |
---|
357 | |
---|
358 | @rtype: dict |
---|
359 | @return: dictionary of host information indexed by hostname derived |
---|
360 | from the map configuration""" |
---|
361 | |
---|
362 | try: |
---|
363 | hosts = self.__srv.getAllHostsInfo() |
---|
364 | |
---|
365 | except httplib.BadStatusLine, e: |
---|
366 | raise AttAuthorityClientError, "HTTP bad status line: %s" % e |
---|
367 | |
---|
368 | except Exception, e: |
---|
369 | # Try to detect exception type from SOAP fault message |
---|
370 | errMsg = str(e) |
---|
371 | for excep in self.excepMap: |
---|
372 | if excep in errMsg: |
---|
373 | raise self.excepMap[excep] |
---|
374 | |
---|
375 | # Catch all |
---|
376 | raise e |
---|
377 | |
---|
378 | # Convert into dictionary form as used by AttAuthority class |
---|
379 | allHostInfo = {} |
---|
380 | for host in hosts: |
---|
381 | hostname = host.Hostname |
---|
382 | |
---|
383 | allHostInfo[hostname] = \ |
---|
384 | { |
---|
385 | 'aaURI': host.AaURI, |
---|
386 | 'aaDN': host.AaDN, |
---|
387 | 'loginURI': host.LoginURI, |
---|
388 | 'loginServerDN': host.LoginServerDN, |
---|
389 | 'loginRequestServerDN': host.LoginRequestServerDN, |
---|
390 | 'role': host.RoleList |
---|
391 | } |
---|
392 | |
---|
393 | return allHostInfo |
---|
394 | |
---|
395 | |
---|
396 | #_________________________________________________________________________ |
---|
397 | def getAttCert(self, userId=None, userCert=None, userAttCert=None): |
---|
398 | """Request attribute certificate from NDG Attribute Authority Web |
---|
399 | Service. |
---|
400 | |
---|
401 | @type userId: string |
---|
402 | @param userId: DN of the X.509 certificate used in SOAP digital |
---|
403 | signature corresponds to the *holder* of the Attribute Certificate |
---|
404 | that is issued. Set this additional field to specify an alternate |
---|
405 | user ID to associate with the AC. This is useful in the case where, |
---|
406 | as in the DEWS project, the holder will be a server cert. rather than |
---|
407 | a user proxy cert. |
---|
408 | |
---|
409 | If this keword is omitted, userId in the AC will default to the same |
---|
410 | value as the holder DN. |
---|
411 | |
---|
412 | @type userCert: string |
---|
413 | @param userCert: certificate corresponding to proxy private key and |
---|
414 | proxy cert used to sign the request. Enables server to establish |
---|
415 | chain of trust proxy -> user cert -> CA cert. If a standard |
---|
416 | private key is used to sign the request, this argument is not |
---|
417 | needed. |
---|
418 | |
---|
419 | @type userAttCert: string / AttCert |
---|
420 | @param userAttCert: user attribute certificate from which to make a |
---|
421 | mapped certificate at the target attribute authority. userAttCert |
---|
422 | must have been issued from a trusted host to the target. This is not |
---|
423 | necessary if the user is registered at the target Attribute Authority. |
---|
424 | |
---|
425 | @rtype ndg.security.common.AttCert.AttCert |
---|
426 | @return attribute certificate for user. iIf access is refused, |
---|
427 | AttributeRequestDenied is raised""" |
---|
428 | |
---|
429 | # Ensure cert is serialized before passing over web service interface |
---|
430 | if isinstance(userAttCert, AttCert): |
---|
431 | userAttCert = userAttCert.toString() |
---|
432 | |
---|
433 | try: |
---|
434 | sAttCert, msg = self.__srv.getAttCert(userId,userCert,userAttCert) |
---|
435 | except httplib.BadStatusLine, e: |
---|
436 | raise AttAuthorityClientError, \ |
---|
437 | 'Calling "%s" HTTP bad status line: %s' % (self.__uri, e) |
---|
438 | |
---|
439 | except Exception, e: |
---|
440 | # Try to detect exception type from SOAP fault message |
---|
441 | errMsg = str(e) |
---|
442 | for excep in self.excepMap: |
---|
443 | if excep in errMsg: |
---|
444 | raise self.excepMap[excep] |
---|
445 | |
---|
446 | # Catch all |
---|
447 | raise e |
---|
448 | |
---|
449 | if sAttCert: |
---|
450 | return AttCertParse(sAttCert) |
---|
451 | else: |
---|
452 | raise AttributeRequestDenied, msg |
---|
453 | |
---|
454 | |
---|
455 | #_________________________________________________________________________ |
---|
456 | def getX509Cert(self): |
---|
457 | """Retrieve the X.509 certificate of the Attribute Authority |
---|
458 | |
---|
459 | @rtype: string |
---|
460 | @return X.509 certificate for Attribute Authority""" |
---|
461 | |
---|
462 | try: |
---|
463 | return self.__srv.getX509Cert() |
---|
464 | except httplib.BadStatusLine, e: |
---|
465 | raise AttAuthorityClientError, "HTTP bad status line: %s" % e |
---|