1 | """NDG Security client - client interface classes to Session Manager |
---|
2 | |
---|
3 | Make requests for authentication and authorisation |
---|
4 | |
---|
5 | NERC Data Grid Project |
---|
6 | |
---|
7 | This software may be distributed under the terms of the Q Public License, |
---|
8 | version 1.0 or later. |
---|
9 | """ |
---|
10 | __author__ = "P J Kershaw" |
---|
11 | __date__ = "24/04/06" |
---|
12 | __copyright__ = "(C) 2007 STFC & NERC" |
---|
13 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |
---|
14 | __revision__ = "$Id:sessionmanager.py 4373 2008-10-29 09:54:39Z pjkersha $" |
---|
15 | |
---|
16 | import logging |
---|
17 | log = logging.getLogger(__name__) |
---|
18 | |
---|
19 | import sys |
---|
20 | import os |
---|
21 | |
---|
22 | # Determine https http transport |
---|
23 | import urlparse |
---|
24 | |
---|
25 | from ZSI.wstools.Utility import HTTPResponse |
---|
26 | |
---|
27 | from ndg.security.common.wssecurity.dom import SignatureHandler |
---|
28 | from ndg.security.common.X509 import * |
---|
29 | from ndg.security.common.AttCert import AttCert, AttCertParse |
---|
30 | from ndg.security.common.m2CryptoSSLUtility import HTTPSConnection, \ |
---|
31 | HostCheck |
---|
32 | from ndg.security.common.zsi.httpproxy import ProxyHTTPConnection |
---|
33 | from ndg.security.common.zsi.sessionmanager.SessionManager_services import \ |
---|
34 | SessionManagerServiceLocator |
---|
35 | |
---|
36 | |
---|
37 | |
---|
38 | class SessionManagerClientError(Exception): |
---|
39 | """Exception handling for SessionManagerClient class""" |
---|
40 | |
---|
41 | class SessionNotFound(SessionManagerClientError): |
---|
42 | """Raise when a session ID input doesn't match with an active session on |
---|
43 | the Session Manager""" |
---|
44 | |
---|
45 | class SessionCertTimeError(SessionManagerClientError): |
---|
46 | """Session's X.509 Cert. not before time is BEFORE the system time - |
---|
47 | usually caused by server's clocks being out of sync. Fix by all servers |
---|
48 | running NTP""" |
---|
49 | |
---|
50 | class SessionExpired(SessionManagerClientError): |
---|
51 | """Session's X.509 Cert. has expired""" |
---|
52 | |
---|
53 | class InvalidSession(SessionManagerClientError): |
---|
54 | """Session is invalid""" |
---|
55 | |
---|
56 | class InvalidSessionManagerClientCtx(SessionManagerClientError): |
---|
57 | """Session Manager ZSI Client is not initialised""" |
---|
58 | |
---|
59 | class AttributeRequestDenied(SessionManagerClientError): |
---|
60 | """Raise when a getAttCert call to the Attribute Authority is denied""" |
---|
61 | |
---|
62 | def __init__(self, *args, **kw): |
---|
63 | """Raise exception for attribute request denied with option to give |
---|
64 | caller hint to certificates that could used to try to obtain a |
---|
65 | mapped certificate |
---|
66 | |
---|
67 | @type extAttCertList: list |
---|
68 | @param extAttCertList: list of candidate Attribute Certificates that |
---|
69 | could be used to try to get a mapped certificate from the target |
---|
70 | Attribute Authority""" |
---|
71 | |
---|
72 | # Prevent None type setting |
---|
73 | self.__extAttCertList = [] |
---|
74 | if 'extAttCertList' in kw and kw['extAttCertList'] is not None: |
---|
75 | for ac in kw['extAttCertList']: |
---|
76 | if isinstance(ac, basestring): |
---|
77 | ac = AttCertParse(ac) |
---|
78 | elif not isinstance(ac, AttCert): |
---|
79 | raise SessionManagerClientError( |
---|
80 | "Input external Attribute Cert. must be AttCert type") |
---|
81 | |
---|
82 | self.__extAttCertList += [ac] |
---|
83 | |
---|
84 | del kw['extAttCertList'] |
---|
85 | |
---|
86 | Exception.__init__(self, *args, **kw) |
---|
87 | |
---|
88 | |
---|
89 | def __getExtAttCertList(self): |
---|
90 | """Return list of candidate Attribute Certificates that could be used |
---|
91 | to try to get a mapped certificate from the target Attribute Authority |
---|
92 | """ |
---|
93 | return self.__extAttCertList |
---|
94 | |
---|
95 | extAttCertList = property(fget=__getExtAttCertList, |
---|
96 | doc="list of candidate Attribute Certificates " |
---|
97 | "that could be used to try to get a mapped " |
---|
98 | "certificate from the target Attribute " |
---|
99 | "Authority") |
---|
100 | |
---|
101 | |
---|
102 | class SessionManagerClient(object): |
---|
103 | """Client interface to Session Manager Web Service |
---|
104 | |
---|
105 | @type excepMap: dict |
---|
106 | @cvar excepMap: map exception strings returned from SOAP fault to client |
---|
107 | Exception class to call""" |
---|
108 | |
---|
109 | excepMap = { |
---|
110 | 'SessionNotFound': SessionNotFound, |
---|
111 | 'UserSessionX509CertNotBeforeTimeError': SessionCertTimeError, |
---|
112 | 'UserSessionExpired': SessionExpired, |
---|
113 | 'InvalidUserSession': InvalidSession |
---|
114 | } |
---|
115 | |
---|
116 | def __init__(self, |
---|
117 | uri=None, |
---|
118 | tracefile=None, |
---|
119 | httpProxyHost=None, |
---|
120 | noHttpProxyList=False, |
---|
121 | sslCACertList=[], |
---|
122 | sslCACertFilePathList=[], |
---|
123 | sslPeerCertCN=None, |
---|
124 | setSignatureHandler=True, |
---|
125 | **signatureHandlerKw): |
---|
126 | """ |
---|
127 | @type uri: string |
---|
128 | @param uri: URI for Session Manager WS. Setting it will set the |
---|
129 | Service user |
---|
130 | |
---|
131 | @type tracefile: file stream type |
---|
132 | @param tracefile: set to file object such as sys.stderr to give extra |
---|
133 | WS debug information |
---|
134 | |
---|
135 | @type sslCACertList: list |
---|
136 | @param sslCACertList: This keyword is for use with SSL connections |
---|
137 | only. Set a list of one or more CA certificates. The peer cert. |
---|
138 | must verify against at least one of these otherwise the connection |
---|
139 | is dropped. |
---|
140 | |
---|
141 | @type sslCACertFilePathList: list |
---|
142 | @param sslCACertFilePathList: the same as the above except CA certs |
---|
143 | can be passed as a list of file paths to read from |
---|
144 | |
---|
145 | @type sslPeerCertCN: string |
---|
146 | @param sslPeerCertCN: set an alternate CommonName to match with peer |
---|
147 | cert. This keyword is for use with SSL connections only. |
---|
148 | |
---|
149 | @type setSignatureHandler: bool |
---|
150 | @param setSignatureHandler: flag to determine whether to apply |
---|
151 | WS-Security Signature Handler or not |
---|
152 | |
---|
153 | @type signatureHandlerKw: dict |
---|
154 | @param signatureHandlerKw: keywords to configure signature handler""" |
---|
155 | |
---|
156 | log.debug("SessionManagerClient.__init__ ...") |
---|
157 | |
---|
158 | self.__srv = None |
---|
159 | self.__uri = None |
---|
160 | self._transdict = {} |
---|
161 | self._transport = ProxyHTTPConnection |
---|
162 | |
---|
163 | if uri: |
---|
164 | self.uri = uri |
---|
165 | |
---|
166 | self.httpProxyHost = httpProxyHost |
---|
167 | self.noHttpProxyList = noHttpProxyList |
---|
168 | |
---|
169 | if sslPeerCertCN: |
---|
170 | self.sslPeerCertCN = sslPeerCertCN |
---|
171 | |
---|
172 | if sslCACertList: |
---|
173 | self.sslCACertList = sslCACertList |
---|
174 | elif sslCACertFilePathList: |
---|
175 | self.sslCACertFilePathList = sslCACertFilePathList |
---|
176 | |
---|
177 | # WS-Security Signature handler - set only if any of the keywords were |
---|
178 | # set |
---|
179 | log.debug("signatureHandlerKw = %s" % signatureHandlerKw) |
---|
180 | if setSignatureHandler: |
---|
181 | self.__signatureHandler = SignatureHandler(**signatureHandlerKw) |
---|
182 | else: |
---|
183 | self.__signatureHandler = None |
---|
184 | |
---|
185 | self.__tracefile = tracefile |
---|
186 | |
---|
187 | |
---|
188 | # Instantiate Session Manager WS ZSI client |
---|
189 | if self.__uri: |
---|
190 | self.initService() |
---|
191 | |
---|
192 | |
---|
193 | def __setURI(self, uri): |
---|
194 | """Set URI for service |
---|
195 | @type uri: string |
---|
196 | @param uri: URI for service to connect to""" |
---|
197 | |
---|
198 | if not isinstance(uri, basestring): |
---|
199 | raise SessionManagerClientError( |
---|
200 | "Session Manager URI must be a valid string") |
---|
201 | |
---|
202 | self.__uri = uri |
---|
203 | try: |
---|
204 | scheme = urlparse.urlparse(self.__uri)[0] |
---|
205 | except TypeError: |
---|
206 | raise AttributeAuthorityClientError( |
---|
207 | "Error parsing transport type from URI: %s" % self.__uri) |
---|
208 | |
---|
209 | if scheme == "https": |
---|
210 | self._transport = HTTPSConnection |
---|
211 | else: |
---|
212 | self._transport = ProxyHTTPConnection |
---|
213 | |
---|
214 | # Ensure SSL settings are cancelled |
---|
215 | self.__setSSLPeerCertCN(None) |
---|
216 | |
---|
217 | def __getURI(self): |
---|
218 | """Get URI for service |
---|
219 | @rtype: string |
---|
220 | @return: uri for service to be invoked""" |
---|
221 | return self.__uri |
---|
222 | |
---|
223 | uri = property(fset=__setURI, fget=__getURI, doc="Session Manager URI") |
---|
224 | |
---|
225 | |
---|
226 | def __setHTTPProxyHost(self, val): |
---|
227 | """Set a HTTP Proxy host overriding any http_proxy environment variable |
---|
228 | setting""" |
---|
229 | if self._transport != ProxyHTTPConnection: |
---|
230 | log.debug("Ignoring httpProxyHost setting: transport class is " |
---|
231 | "not ProxyHTTPConnection type") |
---|
232 | return |
---|
233 | |
---|
234 | self._transdict['httpProxyHost'] = val |
---|
235 | |
---|
236 | httpProxyHost = property(fset=__setHTTPProxyHost, |
---|
237 | doc="HTTP Proxy hostname - overrides any " |
---|
238 | "http_proxy env var setting") |
---|
239 | |
---|
240 | |
---|
241 | def __setNoHttpProxyList(self, val): |
---|
242 | """Set to list of hosts for which to ignore the HTTP Proxy setting""" |
---|
243 | if self._transport != ProxyHTTPConnection: |
---|
244 | log.debug("Ignore noHttpProxyList setting: transport " + \ |
---|
245 | "class is not ProxyHTTPConnection type") |
---|
246 | return |
---|
247 | |
---|
248 | self._transdict['noHttpProxyList']= val |
---|
249 | |
---|
250 | noHttpProxyList = property(fset=__setNoHttpProxyList, |
---|
251 | doc="Set to list of hosts for which to ignore " |
---|
252 | "the HTTP Proxy setting") |
---|
253 | |
---|
254 | |
---|
255 | def __setSSLPeerCertCN(self, cn): |
---|
256 | """For use with HTTPS connections only. Specify the Common |
---|
257 | Name to match with Common Name of the peer certificate. This is not |
---|
258 | needed if the peer cert CN = peer hostname""" |
---|
259 | if self._transport != HTTPSConnection: |
---|
260 | return |
---|
261 | |
---|
262 | if self._transdict.get('postConnectionCheck'): |
---|
263 | self._transdict['postConnectionCheck'].peerCertCN = cn |
---|
264 | else: |
---|
265 | self._transdict['postConnectionCheck'] = HostCheck(peerCertCN=cn) |
---|
266 | |
---|
267 | sslPeerCertCN = property(fset=__setSSLPeerCertCN, |
---|
268 | doc="for https connections, set CN of peer cert " |
---|
269 | "if other than peer hostname") |
---|
270 | |
---|
271 | |
---|
272 | def __setSSLCACertList(self, caCertList): |
---|
273 | """For use with HTTPS connections only. Specify CA certs to one of |
---|
274 | which the peer cert must verify its signature against""" |
---|
275 | if self._transport != HTTPSConnection: |
---|
276 | return |
---|
277 | |
---|
278 | if self._transdict.get('postConnectionCheck'): |
---|
279 | self._transdict['postConnectionCheck'].caCertList = caCertList |
---|
280 | else: |
---|
281 | self._transdict['postConnectionCheck'] = \ |
---|
282 | HostCheck(caCertList=caCertList) |
---|
283 | |
---|
284 | sslCACertList = property(fset=__setSSLCACertList, |
---|
285 | doc="for https connections, set list of CA certs " |
---|
286 | "from which to verify peer cert") |
---|
287 | |
---|
288 | |
---|
289 | def __setSSLCACertFilePathList(self, caCertFilePathList): |
---|
290 | """For use with HTTPS connections only. Specify CA certs to one of |
---|
291 | which the peer cert must verify its signature against""" |
---|
292 | if self._transport != HTTPSConnection: |
---|
293 | return |
---|
294 | |
---|
295 | if self._transdict.get('postConnectionCheck'): |
---|
296 | self._transdict['postConnectionCheck'].caCertFilePathList = \ |
---|
297 | caCertFilePathList |
---|
298 | else: |
---|
299 | self._transdict['postConnectionCheck'] = \ |
---|
300 | HostCheck(caCertFilePathList=caCertFilePathList) |
---|
301 | |
---|
302 | sslCACertFilePathList = property(fset=__setSSLCACertFilePathList, |
---|
303 | doc="for https connections, set list of " |
---|
304 | "CA cert files from which to verify peer " |
---|
305 | "cert") |
---|
306 | |
---|
307 | |
---|
308 | def __setSignatureHandler(self, signatureHandler): |
---|
309 | """Set SignatureHandler object property method - set to None to for no |
---|
310 | digital signature and verification""" |
---|
311 | if signatureHandler is not None and \ |
---|
312 | not isinstance(signatureHandler, SignatureHandler): |
---|
313 | raise AttributeError("Signature Handler must be %s type or None " |
---|
314 | "for no message security" % |
---|
315 | "ndg.security.common.wssecurity.dom.SignatureHandler") |
---|
316 | |
---|
317 | self.__signatureHandler = signatureHandler |
---|
318 | |
---|
319 | def __getSignatureHandler(self): |
---|
320 | "Get SignatureHandler object property method" |
---|
321 | return self.__signatureHandler |
---|
322 | |
---|
323 | signatureHandler = property(fget=__getSignatureHandler, |
---|
324 | fset=__setSignatureHandler, |
---|
325 | doc="SignatureHandler object") |
---|
326 | |
---|
327 | def initService(self, uri=None): |
---|
328 | """Set the WS client for the Session Manager""" |
---|
329 | if uri: |
---|
330 | self.__setURI(uri) |
---|
331 | |
---|
332 | # WS-Security Signature handler object is passed to binding |
---|
333 | try: |
---|
334 | locator = SessionManagerServiceLocator() |
---|
335 | self.__srv = locator.getSessionManager(self.__uri, |
---|
336 | sig_handler=self.__signatureHandler, |
---|
337 | tracefile=self.__tracefile, |
---|
338 | transport=self._transport, |
---|
339 | transdict=self._transdict) |
---|
340 | except HTTPResponse, e: |
---|
341 | raise SessionManagerClientError, \ |
---|
342 | "Initialising Service for \"%s\": %s %s" % \ |
---|
343 | (self.__uri, e.status, e.reason) |
---|
344 | |
---|
345 | def connect(self, |
---|
346 | username, |
---|
347 | passphrase=None, |
---|
348 | passphraseFilePath=None, |
---|
349 | createServerSess=True): |
---|
350 | """Request a new user session from the Session Manager |
---|
351 | |
---|
352 | @type username: string |
---|
353 | @param username: the username of the user to connect |
---|
354 | |
---|
355 | @type passphrase: string |
---|
356 | @param passphrase: user's pass-phrase |
---|
357 | |
---|
358 | @type passphraseFilePath: string |
---|
359 | @param passphraseFilePath: a file containing the user's pass-phrase. |
---|
360 | Use this as an alternative to passphrase keyword. |
---|
361 | |
---|
362 | @type createServerSess: bool |
---|
363 | @param createServerSess: If set to True, the SessionManager will create |
---|
364 | and manage a session for the user. For non-browser client case, it's |
---|
365 | possible to choose to have a client or server side session using this |
---|
366 | keyword. If set to False sessID returned will be None |
---|
367 | |
---|
368 | @rtype: tuple |
---|
369 | @return user cert, user private key, issuing cert and sessID all as |
---|
370 | strings but sessID will be None if the createServerSess keyword is |
---|
371 | False""" |
---|
372 | |
---|
373 | if not self.__srv: |
---|
374 | raise InvalidSessionManagerClientCtx("Client binding is not " |
---|
375 | "initialised") |
---|
376 | |
---|
377 | if passphrase is None: |
---|
378 | try: |
---|
379 | passphrase = open(passphraseFilePath).read().strip() |
---|
380 | |
---|
381 | except Exception, e: |
---|
382 | raise SessionManagerClientError, "Pass-phrase not defined: " + \ |
---|
383 | str(e) |
---|
384 | |
---|
385 | # Make connection |
---|
386 | res = self.__srv.connect(username, passphrase, createServerSess) |
---|
387 | |
---|
388 | # Convert from unicode because unicode causes problems with |
---|
389 | # M2Crypto private key load |
---|
390 | return tuple([isinstance(i,unicode) and str(i) or i for i in res]) |
---|
391 | |
---|
392 | def disconnect(self, userCert=None, sessID=None): |
---|
393 | """Delete an existing user session from the Session Manager |
---|
394 | |
---|
395 | disconnect([userCert=c]|[sessID=i]) |
---|
396 | |
---|
397 | @type userCert: string |
---|
398 | @param userCert: user's certificate used to identifier which session |
---|
399 | to disconnect. This arg is not needed if the message is signed with |
---|
400 | the user cert or if sessID is set. |
---|
401 | |
---|
402 | @type sessID: string |
---|
403 | @param sessID: session ID. Input this as an alternative to userCert |
---|
404 | This arg is not needed if the message is signed with the user cert or |
---|
405 | if userCert keyword is.""" |
---|
406 | |
---|
407 | if not self.__srv: |
---|
408 | raise InvalidSessionManagerClientCtx("Client binding is not " |
---|
409 | "initialised") |
---|
410 | |
---|
411 | # Make connection |
---|
412 | self.__srv.disconnect(userCert, sessID) |
---|
413 | |
---|
414 | def getSessionStatus(self, userDN=None, sessID=None): |
---|
415 | """Check for the existence of a session with a given |
---|
416 | session ID / user certificate Distinguished Name |
---|
417 | |
---|
418 | disconnect([sessID=id]|[userDN=dn]) |
---|
419 | |
---|
420 | @type userCert: string |
---|
421 | @param userCert: user's certificate used to identifier which session |
---|
422 | to disconnect. This arg is not needed if the message is signed with |
---|
423 | the user cert or if sessID is set. |
---|
424 | |
---|
425 | @type sessID: string |
---|
426 | @param sessID: session ID. Input this as an alternative to userCert |
---|
427 | This arg is not needed if the message is signed with the user cert or |
---|
428 | if userCert keyword is.""" |
---|
429 | |
---|
430 | if not self.__srv: |
---|
431 | raise InvalidSessionManagerClientCtx("Client binding is not " |
---|
432 | "initialised") |
---|
433 | |
---|
434 | if sessID and userDN: |
---|
435 | raise SessionManagerClientError( |
---|
436 | 'Only "SessID" or "userDN" keywords may be set') |
---|
437 | |
---|
438 | if not sessID and not userDN: |
---|
439 | raise SessionManagerClientError( |
---|
440 | 'A "SessID" or "userDN" keyword must be set') |
---|
441 | |
---|
442 | # Make connection |
---|
443 | return self.__srv.getSessionStatus(userDN, sessID) |
---|
444 | |
---|
445 | def getAttCert(self, |
---|
446 | userCert=None, |
---|
447 | sessID=None, |
---|
448 | attAuthorityURI=None, |
---|
449 | reqRole=None, |
---|
450 | mapFromTrustedHosts=True, |
---|
451 | rtnExtAttCertList=False, |
---|
452 | extAttCertList=[], |
---|
453 | extTrustedHostList=[]): |
---|
454 | """Request NDG Session Manager Web Service to retrieve an Attribute |
---|
455 | Certificate from the given Attribute Authority and cache it in the |
---|
456 | user's credential wallet held by the session manager. |
---|
457 | |
---|
458 | ac = getAttCert([sessID=i]|[userCert=p][key=arg, ...]) |
---|
459 | |
---|
460 | @raise AttributeRequestDenied: this is raised if the request is |
---|
461 | denied because the user is not registered with the Attribute |
---|
462 | Authority. In this case, a list of candidate attribute certificates |
---|
463 | may be returned which could be used to retry with a request for a |
---|
464 | mapped AC. These are assigned to the raised exception's |
---|
465 | extAttCertList attribute |
---|
466 | |
---|
467 | @type userCert: string |
---|
468 | @param userCert: user certificate - use as ID instead of session |
---|
469 | ID. This can be omitted if the message is signed with a user |
---|
470 | certificate. In this case the user certificate is passed in the |
---|
471 | BinarySecurityToken of the WS-Security header |
---|
472 | |
---|
473 | @type sessID: string |
---|
474 | @param sessID: session ID. Input this as an alternative to |
---|
475 | userCert in the case of a browser client. |
---|
476 | |
---|
477 | @type attAuthorityURI: string |
---|
478 | @param attAuthorityURI: URI for Attribute Authority WS. |
---|
479 | |
---|
480 | @type reqRole: string |
---|
481 | @param reqRole: The required role for access to a data set. This |
---|
482 | can be left out in which case the Attribute Authority just returns |
---|
483 | whatever Attribute Certificate it has for the user |
---|
484 | |
---|
485 | @type mapFromTrustedHosts: bool |
---|
486 | @param mapFromTrustedHosts: Allow a mapped Attribute Certificate to |
---|
487 | be created from a user certificate from another trusted host. |
---|
488 | |
---|
489 | @type rtnExtAttCertList: bool |
---|
490 | @param rtnExtAttCertList: Set this flag True so that if the |
---|
491 | attribute request is denied, a list of potential attribute |
---|
492 | certificates for mapping may be returned. |
---|
493 | |
---|
494 | @type extAttCertList: list |
---|
495 | @param extAttCertList: A list of Attribute Certificates from other |
---|
496 | trusted hosts from which the target Attribute Authority can make a |
---|
497 | mapped certificate |
---|
498 | |
---|
499 | @type extTrustedHostList: list |
---|
500 | @param extTrustedHostList: A list of trusted hosts that can be used |
---|
501 | to get Attribute Certificates for making a mapped AC. |
---|
502 | |
---|
503 | @rtype: ndg.security.common.AttCert.AttCert |
---|
504 | @return: if successful, an attribute certificate.""" |
---|
505 | |
---|
506 | if not self.__srv: |
---|
507 | raise InvalidSessionManagerClientCtx("Client binding is not " |
---|
508 | "initialised") |
---|
509 | |
---|
510 | # Make request |
---|
511 | try: |
---|
512 | attCert, msg, extAttCertList = self.__srv.getAttCert(userCert, |
---|
513 | sessID, |
---|
514 | attAuthorityURI, |
---|
515 | reqRole, |
---|
516 | mapFromTrustedHosts, |
---|
517 | rtnExtAttCertList, |
---|
518 | extAttCertList, |
---|
519 | extTrustedHostList) |
---|
520 | except Exception, e: |
---|
521 | # Try to detect exception type from SOAP fault message |
---|
522 | errMsg = str(e) |
---|
523 | for excep in self.excepMap: |
---|
524 | if excep in errMsg: |
---|
525 | raise self.excepMap[excep] |
---|
526 | |
---|
527 | # Catch all in case none of the known types matched |
---|
528 | raise e |
---|
529 | |
---|
530 | if not attCert: |
---|
531 | raise AttributeRequestDenied(msg, extAttCertList=extAttCertList) |
---|
532 | |
---|
533 | return AttCertParse(attCert) |
---|
534 | |
---|