source: TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/attributeauthority.py @ 6440

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg-security/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/attributeauthority.py@6440
Revision 6440, 19.3 KB checked in by pjkersha, 11 years ago (diff)
  • #1088 Important fix to AuthnRedirectResponseMiddleware? to set redirect ONLY when SSL client authentication has just succeeded in the upstream middleware AuthKitSSLAuthnMiddleware. This bug was causing the browser to redirect to the wrong place following OpenID sign in in the case where the user is already logged into their provider and selects a new relying party to sign into.
    • Improvements to Provider decide page interface: leave out messages about attributes that the provider can't retrieve for the RP. Also included NDG style help icon.
  • Property svn:keywords set to Id
Line 
1"""NDG Security Attribute Authority client - client interface classes to the
2Attribute Authority.  These have been separated from their
3original location in the SecurityClient since they have the
4unusual place of being required by both client and server
5NDG security packages.  For the server side they are required
6as the CredentialWallet invoked by the Session Manager acts as a
7client to Attribute Authorities when negotiating the required
8Attribute Certificate.
9
10Make requests for Attribute Certificates used for authorisation
11
12NERC Data Grid Project
13"""
14__author__ = "P J Kershaw"
15__date__ = "17/11/06"
16__copyright__ = "(C) 2009 Science and Technology Facilities Council"
17__contact__ = "Philip.Kershaw@stfc.ac.uk"
18__license__ = "BSD - see LICENSE file in top-level directory"
19__contact__ = "Philip.Kershaw@stfc.ac.uk"
20__revision__ = "$Id:attributeauthority.py 4373 2008-10-29 09:54:39Z pjkersha $"
21
22import logging
23log = logging.getLogger(__name__)
24
25import traceback
26
27# Determine https http transport
28import urlparse, httplib
29from ZSI.wstools.Utility import HTTPResponse
30
31from ndg.security.common.zsi.attributeauthority.AttributeAuthority_services \
32    import AttributeAuthorityServiceLocator
33from ndg.security.common.wssecurity.signaturehandler.foursuite import \
34    SignatureHandler
35from ndg.security.common.wssecurity.utils import DomletteReader, \
36    DomletteElementProxy
37from ndg.security.common.AttCert import AttCert, AttCertParse
38from ndg.security.common.utils.m2crypto import HTTPSConnection, HostCheck
39from ndg.security.common.zsi.httpproxy import ProxyHTTPConnection
40
41class AttributeAuthorityClientError(Exception):
42    """Exception handling for AttributeAuthorityClient class"""
43
44class AttributeRequestDenied(Exception):
45    """Raise when a getAttCert call to the AA is denied"""
46
47class NoTrustedHosts(AttributeAuthorityClientError):
48    """Raise from getTrustedHosts if there are no trusted hosts defined in
49    the map configuration"""
50
51class NoMatchingRoleInTrustedHosts(AttributeAuthorityClientError):
52    """Raise from getTrustedHosts if there is no mapping to any of the
53    trusted hosts for the given input role name"""
54
55class InvalidAttributeAuthorityClientCtx(AttributeAuthorityClientError):
56    """Attribute Authority ZSI Client is not initialised"""
57
58class AttributeAuthorityClient(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        'AttributeAuthorityNoTrustedHosts': NoTrustedHosts,
67        'AttributeAuthorityNoMatchingRoleInTrustedHosts':
68            NoMatchingRoleInTrustedHosts
69    }
70   
71    def __init__(self, 
72                 uri=None, 
73                 tracefile=None,
74                 httpProxyHost=None,
75                 noHttpProxyList=False,
76                 sslCACertList=[],
77                 sslCACertFilePathList=[],
78                 sslPeerCertCN=None, 
79                 setSignatureHandler=True,
80                 **signatureHandlerKw):
81        """
82        @type uri: string
83        @param uri: URI for Attribute Authority WS.  Setting it will also
84        initialise the Service Proxy
85                                         
86        @param tracefile: set to file object such as sys.stderr to give
87        extra WS debug information
88       
89        @type sslCACertList: list
90        @param sslCACertList: This keyword is for use with SSL connections
91        only.  Set a list of one ore more CA certificates.  The peer cert.
92        must verify against at least one of these otherwise the connection
93        is dropped.
94       
95        @type sslCACertFilePathList: list
96        @param sslCACertFilePathList: the same as the above except CA certs
97        can be passed as a list of file paths to read from
98       
99        @type sslPeerCertCN: string
100        @param sslPeerCertCN: set an alternate CommonName to match with peer
101        cert.  This keyword is for use with SSL connections only.
102                     
103        @type setSignatureHandler: bool
104        @param setSignatureHandler: flag to determine whether to apply
105        WS-Security Signature Handler or not
106
107        @type signatureHandlerKw: dict
108        @param signatureHandlerKw: keywords to configure signature handler"""
109
110        log.debug("AttributeAuthorityClient.__init__ ...")
111        self.__srv = None
112        self.__uri = None
113        self._transdict = {}       
114        self._transport = ProxyHTTPConnection
115       
116        if uri:
117            self.uri = uri
118
119        self.httpProxyHost = httpProxyHost
120        self.noHttpProxyList = noHttpProxyList
121       
122        if sslPeerCertCN:
123            self.sslPeerCertCN = sslPeerCertCN
124       
125        if sslCACertList:
126            self.sslCACertList = sslCACertList
127        elif sslCACertFilePathList:
128            self.sslCACertFilePathList = sslCACertFilePathList
129       
130        # WS-Security Signature handler - set only if any of the keywords were
131        # set
132        if setSignatureHandler:
133            log.debug('signatureHandlerKw = %s' % signatureHandlerKw)
134            self.__signatureHandler = SignatureHandler(**signatureHandlerKw)
135        else:
136            self.__signatureHandler = None
137           
138        self.__tracefile = tracefile
139       
140        # Instantiate Attribute Authority WS proxy
141        if self.uri:
142            self.initService()
143       
144    def __setURI(self, uri):
145        """Set URI for service
146        @type uri: string
147        @param uri: URI for service to connect to"""
148        if not isinstance(uri, basestring):
149            raise AttributeAuthorityClientError(
150                        "Attribute Authority URI must be a valid string")
151       
152        self.__uri = uri
153        try:
154            scheme = urlparse.urlparse(self.__uri)[0]
155        except TypeError:
156            raise AttributeAuthorityClientError(
157                "Error parsing transport type from URI: %s" % self.__uri)
158               
159        if scheme == "https":
160            self._transport = HTTPSConnection
161        else:
162            self._transport = ProxyHTTPConnection
163
164    def __getURI(self):
165        """Get URI for service
166        @rtype: string
167        @return: uri for service to be invoked"""
168        return self.__uri
169           
170    uri = property(fset=__setURI, fget=__getURI,doc="Attribute Authority URI")
171
172    def __setHTTPProxyHost(self, val):
173        """Set a HTTP Proxy host overriding any http_proxy environment variable
174        setting"""
175        if self._transport != ProxyHTTPConnection:
176            log.info("Ignoring httpProxyHost setting: transport class is "
177                     "not ProxyHTTPConnection type")
178            return
179       
180        self._transdict['httpProxyHost'] = val
181
182    httpProxyHost = property(fset=__setHTTPProxyHost, 
183        doc="HTTP Proxy hostname - overrides any http_proxy env var setting")
184
185    def __setNoHttpProxyList(self, val):
186        """Set to list of hosts for which to ignore the HTTP Proxy setting"""
187        if self._transport != ProxyHTTPConnection:
188            log.info("Ignore noHttpProxyList setting: transport class is not "
189                     "ProxyHTTPConnection type")
190            return
191       
192        self._transdict['noHttpProxyList'] = val
193
194    noHttpProxyList = property(fset=__setNoHttpProxyList, 
195                               doc="Set to list of hosts for which to ignore "
196                                   "the HTTP Proxy setting")
197
198    def __setSSLPeerCertCN(self, cn):
199        """For use with HTTPS connections only.  Specify the Common
200        Name to match with Common Name of the peer certificate.  This is not
201        needed if the peer cert CN = peer hostname"""
202        if self._transport != HTTPSConnection:
203            return
204       
205        if self._transdict.get('postConnectionCheck'):
206            self._transdict['postConnectionCheck'].peerCertCN = cn
207        else:
208            self._transdict['postConnectionCheck'] = HostCheck(peerCertCN=cn)
209
210    sslPeerCertCN = property(fset=__setSSLPeerCertCN, 
211                             doc="for https connections, set CN of peer cert "
212                                 "if other than peer hostname")
213
214    def __setSSLCACertList(self, caCertList):
215        """For use with HTTPS connections only.  Specify CA certs to one of
216        which the peer cert must verify its signature against"""
217        if self._transport != HTTPSConnection:
218            return
219       
220        if self._transdict.get('postConnectionCheck'):
221            self._transdict['postConnectionCheck'].caCertList = caCertList
222        else:
223            self._transdict['postConnectionCheck'] = \
224                                            HostCheck(caCertList=caCertList)
225
226    sslCACertList = property(fset=__setSSLCACertList, 
227                             doc="for https connections, set list of CA "
228                                 "certs from which to verify peer cert")
229
230    def __setSSLCACertFilePathList(self, caCertFilePathList):
231        """For use with HTTPS connections only.  Specify CA certs to one of
232        which the peer cert must verify its signature against"""
233        if self._transport != HTTPSConnection:
234            return
235       
236        if self._transdict.get('postConnectionCheck'):
237            self._transdict['postConnectionCheck'].caCertFilePathList = \
238                                                            caCertFilePathList
239        else:
240            self._transdict['postConnectionCheck'] = \
241                            HostCheck(caCertFilePathList=caCertFilePathList)
242
243    sslCACertFilePathList = property(fset=__setSSLCACertFilePathList, 
244                                     doc="for https connections, set list of "
245                                         "CA certificate files from which to "
246                                         "verify peer certificate")
247
248    def __setSignatureHandler(self, signatureHandler):
249        """Set SignatureHandler object property method - set to None to for no
250        digital signature and verification"""
251        if not isinstance(signatureHandler, (signatureHandler,None.__class__)):
252            raise AttributeError("Signature Handler must be %s type or None "
253                                 "for no message security" % SignatureHandler)
254                           
255        self.__signatureHandler = signatureHandler
256   
257    def __getSignatureHandler(self):
258        "Get SignatureHandler object property method"
259        return self.__signatureHandler
260   
261    signatureHandler = property(fget=__getSignatureHandler,
262                                fset=__setSignatureHandler,
263                                doc="SignatureHandler object")
264   
265    def initService(self, uri=None):
266        """Set the WS proxy for the Attribute Authority
267       
268        @type uri: string
269        @param uri: URI for service to invoke"""
270       
271        if uri:
272            self.__setURI(uri)
273
274        # WS-Security Signature handler object is passed to binding
275        try:
276            locator = AttributeAuthorityServiceLocator()
277            self.__srv = locator.getAttributeAuthority(self.__uri, 
278                                         sig_handler=self.__signatureHandler,
279                                         readerclass=DomletteReader, 
280                                         writerclass=DomletteElementProxy,
281                                         tracefile=self.__tracefile,
282                                         transport=self._transport,
283                                         transdict=self._transdict)
284        except HTTPResponse, e:
285            raise AttributeAuthorityClientError(
286                "Error initialising service for \"%s\": %s %s" % 
287                (self.__uri, e.status, e.reason))
288
289    def getHostInfo(self):
290        """Get host information for the data provider which the
291        Attribute Authority represents
292       
293        @rtype: dict
294        @return: dictionary of host information derived from the Attribute
295        Authority's map configuration
296        """
297   
298        if not self.__srv:
299            raise InvalidAttributeAuthorityClientCtx("Client binding is not "
300                                                     "initialised")
301
302        try:
303            # Convert return tuple into list to enable use of pop() later
304            response = list(self.__srv.getHostInfo())
305        except httplib.BadStatusLine, e:
306            raise AttributeAuthorityClientError("HTTP bad status line: %s" % e)
307
308        except Exception, e:
309            log.error("AttributeAuthority.getHostInfo: %s", 
310                      traceback.format_exc())
311           
312            # Try to detect exception type from SOAP fault message
313            errMsg = str(e)
314            for excep in self.excepMap:
315                if excep in errMsg:
316                    raise self.excepMap[excep]
317               
318            # Catch all   
319            raise e
320
321        # Unpack response into dict
322        hostInfoKw = ['siteName',
323                      'aaURI',
324                      'aaDN',
325                      'loginURI',
326                      'loginServerDN',
327                      'loginRequestServerDN']
328        hostInfoKw.reverse()
329        hostInfo = {response.pop(): \
330                    dict([(k, response.pop()) for k in hostInfoKw])}
331
332        return hostInfo
333
334    def getTrustedHostInfo(self, role=None):
335        """Get list of trusted hosts for an Attribute Authority
336       
337        @type role: string
338        @param role: get information for trusted hosts that have a mapping to
339        this role
340       
341        @rtype: dict
342        @return: dictionary of host information indexed by hostname derived
343        from the map configuration"""
344   
345        if not self.__srv:
346            raise InvalidAttributeAuthorityClientCtx("Client binding is not "
347                                                     "initialised")
348           
349        try:
350            trustedHosts = self.__srv.getTrustedHostInfo(role)
351
352        except httplib.BadStatusLine, e:
353            raise AttributeAuthorityClientError("HTTP bad status line: %s" % e)
354
355        except Exception, e:
356            log.error("AttributeAuthority.getTrustedHostInfo: %s", 
357                      traceback.format_exc())
358           
359            # Try to detect exception type from SOAP fault message
360            errMsg = str(e)
361            for excep in self.excepMap:
362                if excep in errMsg:
363                    raise self.excepMap[excep]
364               
365            # Catch all   
366            raise e
367
368        # Convert into dictionary form as used by AttributeAuthority class
369        trustedHostInfo = {}
370        for host in trustedHosts:
371            hostname = host.get_element_hostname()
372           
373            trustedHostInfo[hostname] = {
374                'siteName': host.SiteName,
375                'aaURI': host.AaURI,
376                'aaDN': host.AaDN,
377                'loginURI': host.LoginURI,
378                'loginServerDN': host.LoginServerDN,
379                'loginRequestServerDN': host.LoginRequestServerDN,
380                'role': host.RoleList
381            }
382           
383        return trustedHostInfo
384
385    def getAllHostsInfo(self):
386        """Get list of all hosts for an Attribute Authority i.e. itself and
387        all the hosts it trusts
388       
389        @rtype: dict
390        @return: dictionary of host information indexed by hostname derived
391        from the map configuration"""
392   
393        if not self.__srv:
394            raise InvalidAttributeAuthorityClientCtx("Client binding is not "
395                                                     "initialised")
396
397        hosts = self.__srv.getAllHostsInfo()
398        try:
399            hosts = self.__srv.getAllHostsInfo()
400
401        except httplib.BadStatusLine, e:
402            raise AttributeAuthorityClientError("HTTP bad status line: %s" % e)
403
404        except Exception, e:
405            log.error("AttributeAuthority.getAllHostsInfo: %s", 
406                      traceback.format_exc())
407           
408            # Try to detect exception type from SOAP fault message
409            errMsg = str(e)
410            for excep in self.excepMap:
411                if excep in errMsg:
412                    raise self.excepMap[excep]
413               
414            # Catch all   
415            raise e
416
417        # Convert into dictionary form as used by AttributeAuthority class
418        allHostInfo = {}
419        for host in hosts:
420            hostname = host.Hostname
421            allHostInfo[hostname] = {
422                'siteName': host.SiteName,
423                'aaURI': host.AaURI,
424                'aaDN': host.AaDN,
425                'loginURI': host.LoginURI,
426                'loginServerDN': host.LoginServerDN,
427                'loginRequestServerDN': host.LoginRequestServerDN,
428                'role': host.RoleList
429            }
430
431        return allHostInfo   
432
433    def getAttCert(self, userId=None, userX509Cert=None, userAttCert=None):
434        """Request attribute certificate from NDG Attribute Authority Web
435        Service.
436       
437        @type userId: string
438        @param userId: DN of the X.509 certificate used in SOAP digital
439        signature corresponds to the *holder* of the Attribute Certificate
440        that is issued.  Set this additional field to specify an alternate
441        user ID to associate with the AC.  This is useful in the case where,
442        as in the DEWS project, the holder will be a server cert. rather than
443        a user cert.
444       
445        If this keword is omitted, userId in the AC will default to the same
446        value as the holder DN.
447       
448        @type userX509Cert: string
449        @param userX509Cert: certificate corresponding to proxy private key and
450        proxy cert used to sign the request.  Enables server to establish
451        chain of trust proxy -> user cert -> CA cert.  If a standard
452        private key is used to sign the request, this argument is not
453        needed.
454       
455        @type userAttCert: string / AttCert
456        @param userAttCert: user attribute certificate from which to make a
457        mapped certificate at the target attribute authority.  userAttCert
458        must have been issued from a trusted host to the target.  This is not
459        necessary if the user is registered at the target Attribute Authority.
460       
461        @rtype ndg.security.common.AttCert.AttCert
462        @return attribute certificate for user.  If access is refused,
463        AttributeRequestDenied is raised"""
464   
465        if not self.__srv:
466            raise InvalidAttributeAuthorityClientCtx("Client binding is not "
467                                                     "initialised")
468
469        # Ensure cert is serialized before passing over web service interface
470        if isinstance(userAttCert, AttCert):
471            userAttCert = userAttCert.toString()
472
473        try:
474            sAttCert, msg = self.__srv.getAttCert(userId,
475                                                  userX509Cert,
476                                                  userAttCert) 
477        except httplib.BadStatusLine, e:
478            raise AttributeAuthorityClientError(
479                'Calling "%s" HTTP bad status line: %s' % (self.__uri, e))
480
481        except Exception, e:
482            log.error("AttributeAuthority.getAttCert: %s", 
483                      traceback.format_exc())
484           
485            # Try to detect exception type from SOAP fault message
486            errMsg = str(e)
487            for excep in self.excepMap:
488                if excep in errMsg:
489                    raise self.excepMap[excep]
490               
491            # Catch all   
492            raise e
493       
494        if sAttCert:
495            return AttCertParse(sAttCert)
496        else:
497            raise AttributeRequestDenied(msg)
Note: See TracBrowser for help on using the repository browser.