source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/sessionmanager.py @ 4692

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/sessionmanager.py@4692
Revision 4692, 21.5 KB checked in by pjkersha, 11 years ago (diff)

Refactoring of SSO service to enable use of local AA and SM instances via keys to environ.

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