source: TI12-security/trunk/python/ndg_security_common/ndg/security/common/credentialwallet.py @ 6033

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_common/ndg/security/common/credentialwallet.py@6033
Revision 6033, 92.4 KB checked in by pjkersha, 10 years ago (diff)

Refactoring Credential Wallet to enable caching of SAML assertions.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""Credential Wallet classes
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "30/11/05"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__license__ = "BSD - see LICENSE file in top-level directory"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = '$Id:credentialwallet.py 4378 2008-10-29 10:30:14Z pjkersha $'
11
12import logging
13log = logging.getLogger(__name__)
14
15import traceback
16
17# Check Attribute Certificate validity times
18from datetime import datetime, timedelta
19
20# Access Attribute Authority's web service using ZSI - allow pass if not
21# loaded since it's possible to make AttributeAuthority instance locally
22# without using the WS
23aaImportError = True
24try:
25    # AttributeAuthority client package resides with CredentialWallet module in
26    # ndg.security.common
27    from ndg.security.common.attributeauthority import (
28        AttributeAuthorityClient, AttributeAuthorityClientError, 
29        AttributeRequestDenied, NoMatchingRoleInTrustedHosts)
30    aaImportError = False
31except ImportError:
32    log.warning('Loading CredentialWallet without SOAP interface imports')
33
34# Likewise - may not want to use WS and use AttributeAuthority locally in which
35# case no need to import it
36try:
37    from ndg.security.server.attributeauthority import (AttributeAuthority, 
38        AttributeAuthorityError, AttributeAuthorityAccessDenied)
39    aaImportError = False
40except:
41    log.warning('Loading CredentialWallet without Attribute Authority '
42                'interface imports')
43
44if aaImportError:
45    raise ImportError("Either AttributeAuthority or AttributeAuthorityClient "
46                      "classes must be present to allow interoperation with "
47                      "Attribute Authorities")
48
49# Authentication X.509 Certificate
50from ndg.security.common.X509 import *
51from M2Crypto import X509, BIO, RSA
52
53# Authorisation - attribute certificate
54from ndg.security.common.AttCert import *
55from ndg.security.common.wssecurity.signaturehandler.dom import SignatureHandler
56
57# generic parser to read INI/XML properties file
58from ndg.security.common.utils.configfileparsers import \
59                                                INIPropertyFileWithValidation
60
61
62class _CredentialWalletException(Exception):   
63    """Generic Exception class for CredentialWallet module.  Overrides
64    Exception to enable writing to the log"""
65    def __init__(self, msg):
66        log.error(msg)
67        Exception.__init__(self, msg)
68
69
70class CredentialWalletError(_CredentialWalletException):   
71    """Exception handling for NDG Credential Wallet class.  Overrides Exception
72    to enable writing to the log"""
73
74
75class CredentialWalletAttributeRequestDenied(CredentialWalletError):   
76    """Handling exception where CredentialWallet is denied authorisation by an
77    Attribute Authority.
78 
79    @type __extAttCertList: list
80    @ivar __extAttCertList: list of candidate Attribute Certificates that
81    could be used to try to get a mapped certificate from the target
82    Attribute Authority
83   
84    @type __trustedHostInfo: dict
85    @ivar __trustedHostInfo: dictionary indexed by host name giving
86    details of Attribute Authority URI and roles for trusted hosts"""
87   
88    def __init__(self, *args, **kw):
89        """Raise exception for attribute request denied with option to give
90        caller hint to certificates that could used to try to obtain a
91        mapped certificate
92       
93        @type extAttCertList: list
94        @param extAttCertList: list of candidate Attribute Certificates that
95        could be used to try to get a mapped certificate from the target
96        Attribute Authority
97       
98        @type trustedHostInfo: dict
99        @param trustedHostInfo: dictionary indexed by host name giving
100        details of Attribute Authority URI and roles for trusted hosts"""
101       
102        self.__trustedHostInfo = kw.pop('trustedHostInfo', {})
103        self.__extAttCertList = kw.pop('extAttCertList', [])
104           
105        CredentialWalletError.__init__(self, *args, **kw)
106
107    def _getTrustedHostInfo(self):
108        """Get message text"""
109        return self.__trustedHostInfo
110
111    trustedHostInfo = property(fget=_getTrustedHostInfo, 
112                               doc="URI and roles details for trusted hosts")
113       
114    def _getExtAttCertList(self):
115        """Return list of candidate Attribute Certificates that could be used
116        to try to get a mapped certificate from the target Attribute Authority
117        """
118        return self.__extAttCertList
119
120    extAttCertList = property(fget=_getExtAttCertList,
121                              doc="list of candidate Attribute Certificates "
122                              "that could be used to try to get a mapped "
123                              "certificate from the target Attribute "
124                              "Authority")
125
126         
127class _MetaCredentialWallet(type):
128    """Enable CredentialWallet to have read only class variables e.g.
129   
130    print CredentialWallet.accessDenied
131   
132    ... is allowed but,
133   
134    CredentialWallet.accessDenied = None
135   
136    ... raises - AttributeError: can't set attribute"""
137   
138    def _getAccessDenied(cls):
139        '''accessDenied get method'''
140        return False
141   
142    accessDenied = property(fget=_getAccessDenied)
143   
144    def _getAccessGranted(cls):
145        '''accessGranted get method'''
146        return True
147   
148    accessGranted = property(fget=_getAccessGranted)
149
150
151class CredentialWalletBase(object):
152    """Abstract base class for NDG and SAML Credential Wallet implementations
153    """
154    __slots__ = (
155        "userId",
156        "attributeAuthorityURI"
157        "credentials",
158        "credentialsKeyedByURI",
159    )
160    __slots__ += tuple(["_CredentialWalletBase__%s" % name
161                        for name in __slots__])
162    del name
163   
164    def __init__(self):
165        self.__userId = None
166        self.__attributeAuthorityURI = None
167        self.__credentials = {}
168        self.__credentialsKeyedByURI = {}
169
170    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
171        '''Virtual method defines interface to read config file settings
172        @type cfg: basestring /ConfigParser derived type
173        @param cfg: configuration file path or ConfigParser type object
174        @type prefix: basestring
175        @param prefix: prefix for option names e.g. "certExtApp."
176        @type section: baestring
177        @param section: configuration file section from which to extract
178        parameters.
179        '''
180        raise NotImplementedError(CredentialWalletBase.parseConfig.__doc__)
181
182    def addCredential(self, 
183                      credential, 
184                      attributeAuthorityURI=None,
185                      bUpdateCredentialRepository=True):
186        """Add a new attribute certificate to the list of credentials held.
187
188        @type credential: determined by derived class implementation e.g.
189        SAML assertion
190        @param credential: new attribute Certificate to be added
191        @type attributeAuthorityURI: basestring
192        @param attributeAuthorityURI: input the Attribute Authority URI from
193        which credential was retrieved.  This is added to a dict to enable
194        access to a given Attribute Certificate keyed by Attribute Authority
195        URI. See the getCredential method.
196        @type bUpdateCredentialRepository: bool
197        @param bUpdateCredentialRepository: if set to True, and a repository
198        exists it will be updated with the new credentials also
199       
200        @rtype: bool
201        @return: True if certificate was added otherwise False.  - If an
202        existing certificate from the same issuer has a later expiry it will
203        take precedence and the new input certificate is ignored."""
204        raise NotImplementedError(BaseCredentialWallet.addCredential.__doc__)
205           
206    def audit(self):
207        """Check the credentials held in the wallet removing any that have
208        expired or are otherwise invalid."""
209        raise NotImplementedError(BaseCredentialWallet.audit.__doc__)
210
211    def updateCredentialRepository(self, auditCred=True):
212        """Copy over non-persistent credentials held by wallet into the
213        perminent repository.
214       
215        @type auditCred: bool
216        @param auditCred: filter existing credentials in the repository
217        removing invalid ones"""
218        raise NotImplementedError(
219                    BaseCredentialWallet.updateCredentialRepository.__doc__)
220       
221    def _getCredentials(self):
222        """Get Property method.  Credentials dict is read-only but also see
223        addCredential method
224       
225        @rtype: dict
226        @return: cached ACs indesed by issuing organisation name"""
227        return self.__credentials
228
229    # Publish attribute
230    credentials = property(fget=_getCredentials,
231                           doc="List of credentials linked to issuing "
232                               "authorities")
233
234    def _getCredentialsKeyedByURI(self):
235        """Get Property method for credentials keyed by Attribute Authority URI
236        Credentials dict is read-only but also see addCredential method
237       
238        @rtype: dict
239        @return: cached ACs indexed by issuing Attribute Authority"""
240        return self.__credentialsKeyedByURI
241   
242    # Publish attribute
243    credentialsKeyedByURI = property(fget=_getCredentialsKeyedByURI,
244                                     doc="List of Attribute Certificates "
245                                         "linked to attribute authority URI")
246       
247    def _getUserId(self):
248        return self.__userId
249
250    def _setUserId(self, value):
251        if not isinstance(value, basestring):
252            raise TypeError('Expecting string type for "userId"; got %r '
253                            'instead' % type(value))
254        self.__userId = value
255
256    userId = property(_getUserId, _setUserId, 
257                      doc="User Identity for this wallet")
258
259    def _getAttributeAuthorityURI(self):
260        return self.__attributeAuthorityURI
261
262    def _setAttributeAuthorityURI(self, value):
263        if not isinstance(value, basestring):
264            raise TypeError('Expecting string type for "attributeAuthorityURI";'
265                            ' got %r instead' % type(value))
266        self.__attributeAuthorityURI = value
267
268    attributeAuthorityURI = property(_getAttributeAuthorityURI,
269                                     _setAttributeAuthorityURI, 
270                                     doc="Attribute Authority Service URI")
271
272    def __getstate__(self):
273        '''Enable pickling for use with beaker.session'''
274        return dict([(attrName, getattr(self, attrName))
275                     for attrName in self.__class__.__slots__])
276       
277    def __setstate__(self, attrDict):
278        '''Enable pickling for use with beaker.session'''
279        for attr, val in attrDict.items():
280            setattr(self, attr, val)
281           
282   
283class CredentialWallet(CredentialWalletBase):
284    """Volatile store of user credentials associated with a user session
285   
286    @type userX509Cert: string / M2Crypto.X509.X509 /
287    ndg.security.common.X509.X509Cert
288    @ivar userX509Cert: X.509 certificate for user (property attribute)
289   
290    @type userPriKey: string / M2Crypto.RSA.RSA
291    @ivar userPriKey: private key for user cert (property attribute)
292   
293    @type issuingX509Cert: string / ndg.security.common.X509.X509Cert
294    @ivar issuingX509Cert: X.509 cert for issuer of user cert (property
295    attribute)
296   
297    @type attributeAuthorityURI: string
298    @ivar attributeAuthorityURI: URI of Attribute Authority to make
299    requests to.  Setting this ALSO creates an AttributeAuthorityClient
300    instance _attributeAuthorityClnt.  - See attributeAuthorityURI property for
301    details. (property attribute)
302   
303    @type attributeAuthority: ndg.security.server.attributeauthority.AttributeAuthority
304    @ivar attributeAuthority: Attribute Authority to make requests to. 
305    attributeAuthorityURI takes precedence over this keyword i.e. if an
306    attributeAuthorityURI has been set, then calls are made to the AA web
307    service at this location rather to any self.attributeAuthority running
308    locally. (property attribute)
309   
310    @type caCertFilePathList: string (for single file), list or tuple
311    @ivar caCertFilePathList: Certificate Authority's certificates - used
312    in validation of signed Attribute Certificates and WS-Security
313    signatures of incoming messages.  If not set here, it must
314    be input in call to getAttCert. (property attribute)
315           
316    @type credentialRepository: instance of CredentialRepository derived
317    class
318    @ivar credentialRepository: Credential Repository instance.   (property
319    attribute).  If not set, defaults to NullCredentialRepository type - see
320    class below...
321
322   
323    @type mapFromTrustedHosts: bool
324    @ivar mapFromTrustedHosts sets behaviour for getAttCert().  If
325    set True and authorisation fails with the given Attribute Authority,
326    attempt to get authorisation using Attribute Certificates issued by
327    other trusted AAs. (property attribute)
328   
329    @type rtnExtAttCertList: bool
330    @ivar rtnExtAttCertList: behaviour for getAttCert().  If True, and
331    authorisation fails with the given Attribute Authority, return a list
332    of Attribute Certificates from other trusted AAs which could be used
333    to obtain a mapped Attribute Certificate on a subsequent authorisation
334    attempt. (property attribute)
335   
336    @type attCertRefreshElapse: float / int
337    @ivar attCertRefreshElapse: used by getAttCert to determine
338    whether to replace an existing AC in the cache with a fresh one.  If
339    the existing one has less than attCertRefreshElapse time in seconds
340    left before expiry then replace it. (property attribute)
341   
342    @type wssCfgKw: dict
343    @ivar wssCfgKw: keywords to WS-Security SignatureHandler
344    used for Credential Wallet's SOAP interface to Attribute Authorities.
345    (property attribute)
346           
347    @type _credentialRepository: ndg.security.common.CredentialRepository or
348    derivative
349    @ivar _credentialRepository: reference to Credential Repository object. 
350    An optional non-volatile cache for storage of wallet info which can be
351    later restored. (Don't reference directly - see equivalent property
352    attribute)
353
354    @type _mapFromTrustedHosts: bool
355    @ivar _mapFromTrustedHosts: if true, allow a mapped attribute certificate
356    to obtained in a getAttCert call.  Set false to prevent mappings.
357    (Don't reference directly - see equivalent property attribute)
358
359    @type _rtnExtAttCertList: bool
360    @ivar _rtnExtAttCertList: if true, return a list of external attribute
361    certificates from getAttCert call. (Don't reference directly - see
362    equivalent property attribute)
363
364    @type __dn: ndg.security.common.X509.X500DN
365    @ivar __dn: distinguished name from user certificate.  (Don't reference
366    directly - see equivalent property attribute)
367
368    @type _credentials: dict       
369    @ivar _credentials: Credentials are stored as a dictionary one element per
370    attribute certificate held and indexed by certificate issuer name.
371    (Don't reference directly - see equivalent property attribute)
372
373    @type _caCertFilePathList: basestring, list, tuple or None
374    @ivar _caCertFilePathList: file path(s) to CA certificates.  If None
375    then the input is quietly ignored.  See caCertFilePathList property.
376    (Don't reference directly - see equivalent property attribute)
377
378    @type _userX509Cert: ndg.security.common.X509.X509Cert
379    @ivar _userX509Cert: X.509 user certificate instance.
380    (Don't reference directly - see equivalent property attribute)
381
382    @type _issuingX509Cert: ndg.security.common.X509.X509Cert
383    @ivar _issuingX509Cert: X.509 user certificate instance.
384    (Don't reference directly - see equivalent property attribute)
385 
386    @type _userPriKey: M2Crypto.RSA.RSA
387    @ivar _userPriKey: Private key used to sign outbound message.
388    (Don't reference directly - see equivalent property attribute)
389    """
390
391    __metaclass__ = _MetaCredentialWallet
392
393    propertyDefaults = dict(
394        userX509Cert=None,
395        userX509CertFilePath=None,
396        userPriKey=None,
397        userPriKeyFilePath=None,
398        issuingX509Cert=None,
399        issuingX509CertFilePath=None,
400        caCertFilePathList=[],
401        sslCACertFilePathList=[],
402        attributeAuthorityURI=None,
403        attributeAuthority=None,
404        credentialRepository=None,
405        mapFromTrustedHosts=False,
406        rtnExtAttCertList=True,
407        attCertRefreshElapse=7200,
408        wssCfgFilePath=None,
409        wssCfgSection='DEFAULT',
410        wssCfgPrefix='',
411        wssCfgKw={})
412   
413    _protectedAttrs = [
414        '_userX509Cert',
415        '_userX509CertFilePath',
416        '_userPriKey',
417        '_userPriKeyFilePath',
418        '_userPriKeyPwd',
419        '_issuingX509Cert',
420        '_issuingX509CertFilePath',
421        '_attributeAuthorityClnt',
422        '_attributeAuthority',
423        '_caCertFilePathList',
424        '_mapFromTrustedHosts',
425        '_rtnExtAttCertList',
426        '_attCertRefreshElapse',
427        '_cfg',
428        '_dn'
429    ]
430   
431    __slots__ = propertyDefaults.keys() + _protectedAttrs
432   
433    def __init__(self, 
434                 cfg=None, 
435                 cfgFileSection='DEFAULT', 
436                 cfgPrefix='', 
437                 wssCfgKw={},
438                 **kw):
439        """Create store of user credentials for their current session
440
441        @type cfg: string / ConfigParser object
442        @param cfg: if a string type, this is interpreted as the file path to
443        a configuration file, otherwise it will be treated as a ConfigParser
444        object
445        @type cfgSection: string
446        @param cfgSection: sets the section name to retrieve config params
447        from
448        @type cfgPrefix: basestring
449        @param cfgPrefix: apply a prefix to all CredentialWallet config params
450        so that if placed in a file with other parameters they can be
451        distinguished
452        @type cfgKw: dict
453        @param cfgKw: set parameters as key value pairs."""
454
455        log.debug("Calling CredentialWallet.__init__ ...")
456
457        super(CredentialWallet, self).__init__()
458       
459        # Initialise attributes - 1st protected ones
460        attr = {}.fromkeys(CredentialWallet._protectedAttrs)
461       
462        # ... then properties
463        attr.update(CredentialWallet.propertyDefaults)
464        for k, v in attr.items():
465            setattr(self, k, v)
466           
467        # Update attributes from a config file
468        if cfg:
469            self.parseConfig(cfg, section=cfgFileSection, prefix=cfgPrefix)
470
471        # Update attributes from keywords passed - set user private key
472        # password first if it's present.  This is to avoid an error setting
473        # the private key
474        self.userPriKeyPwd = kw.pop('userPriKeyPwd', None)
475        for k,v in kw.items():
476            setattr(self, k, v)
477
478        # Get the distinguished name from the user certificate
479        if self._userX509Cert:
480            self._dn = self._userX509Cert.dn.serialise()
481
482        # Make a connection to the Credentials Repository
483        if self.credentialRepository is None:
484            log.info('Applying default CredentialRepository %r for user '
485                     '"%s"' % (NullCredentialRepository, self.userId))
486            self.credentialRepository = NullCredentialRepository()
487        else:
488            log.info('Checking CredentialRepository for credentials for user '
489                     '"%s"' % self.userId)
490           
491            if not issubclass(self.credentialRepository, CredentialRepository):
492                raise CredentialWalletError("Input Credential Repository "
493                                            "instance must be of a class "
494                                            "derived from "
495                                            "\"CredentialRepository\"")
496   
497       
498            # Check for valid attribute certificates for the user
499            try:
500                self.credentialRepository.auditCredentials(self.userId)
501                userCred=self.credentialRepository.getCredentials(self.userId)
502   
503            except Exception, e:
504                log.error("Error updating wallet with credentials from "
505                          "repository: %s" % e)
506                raise
507   
508   
509            # Update wallet with attribute certificates stored in the
510            # repository.  Store ID and certificate instantiated as an AttCert
511            # type
512            try:
513                for cred in userCred: 
514                    attCert = AttCertParse(cred.attCert)
515                    issuerName = attCert['issuerName']
516                   
517                    self.credentials[issuerName] = {'id':cred.id, 
518                                                     'attCert':attCert}   
519            except Exception, e:
520                try:
521                    raise CredentialWalletError("Error parsing Attribute "
522                        "Certificate ID '%s' retrieved from the " 
523                        "Credentials Repository: %s" % (cred.id, e))           
524                except:
525                    raise CredentialWalletError("Error parsing Attribute "
526                                          "Certificate retrieved from the "
527                                          "Credentials Repository: %s:" % e)
528           
529            # Filter out expired or otherwise invalid certificates
530            self.audit()
531   
532    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
533        '''Extract parameters from cfg config object'''
534       
535        if isinstance(cfg, basestring):
536            cfgFilePath = os.path.expandvars(cfg)
537            self._cfg = None
538        else:
539            cfgFilePath = None
540            self._cfg = cfg
541           
542        # Configuration file properties are held together in a dictionary
543        readAndValidate = INIPropertyFileWithValidation()
544        prop = readAndValidate(cfgFilePath,
545                               cfg=self._cfg,
546                               validKeys=CredentialWallet.propertyDefaults,
547                               prefix=prefix,
548                               sections=(section,))
549       
550        # Keep a copy of config for use by WS-Security SignatureHandler parser
551        if self._cfg is None:
552            self._cfg = readAndValidate.cfg
553       
554        # Copy prop dict into object attributes - __slots__ definition and
555        # property methods will ensure only the correct attributes are set
556        # Set user private key password first if it's present.  This is to
557        # avoid an error setting the private key
558        self.userPriKeyPwd = prop.pop('userPriKeyPwd', None)
559        for key, val in prop.items():
560            setattr(self, key, val)
561
562
563    def _getAttCertRefreshElapse(self):
564        """Get property method for Attribute Certificate wallet refresh time
565        @rtype: float or int
566        @return: "elapse time in seconds"""
567        return self._attCertRefreshElapse
568   
569    def _setAttCertRefreshElapse(self, val):
570        """Set property method for Attribute Certificate wallet refresh time
571        @type val: float or int
572        @param val: "elapse time in seconds"""
573        if isinstance(val, (float, int)):
574            self._attCertRefreshElapse = val
575           
576        elif isinstance(val, basestring):
577            self._attCertRefreshElapse = float(val)
578        else:
579            raise AttributeError("Expecting int, float or string type input "
580                                 "for attCertRefreshElapse")
581           
582    attCertRefreshElapse = property(fget=_getAttCertRefreshElapse, 
583                                    fset=_setAttCertRefreshElapse,
584                                    doc="If an existing one has AC less than "
585                                        "attCertRefreshElapse time in seconds "
586                                        "left before expiry then replace it")
587   
588    def _setX509Cert(self, cert):
589        """filter and convert input cert to signing verifying cert set
590        property methods.  For signingCert, set to None if it is not to be
591        included in the SOAP header.  For verifyingCert, set to None if this
592        cert can be expected to be retrieved from the SOAP header of the
593        message to be verified
594       
595        @type: ndg.security.common.X509.X509Cert / M2Crypto.X509.X509 /
596        string or None
597        @param cert: X.509 certificate. 
598       
599        @rtype ndg.security.common.X509.X509Cert
600        @return X.509 certificate object"""
601       
602        if cert is None or isinstance(cert, X509Cert):
603            # ndg.security.common.X509.X509Cert type / None
604            return cert
605           
606        elif isinstance(cert, X509.X509):
607            # M2Crypto.X509.X509 type
608            return X509Cert(m2CryptoX509=cert)
609           
610        elif isinstance(cert, basestring):
611            return X509CertParse(cert)
612       
613        else:
614            raise AttributeError("X.509 Cert. must be type: "
615                                 "ndg.security.common.X509.X509Cert, "
616                                 "M2Crypto.X509.X509 or a base64 encoded "
617                                 "string")
618
619    def _setUserX509Cert(self, userX509Cert):
620        "Set property method for X.509 user cert."
621        self._userX509Cert = self._setX509Cert(userX509Cert)       
622
623    def _getUserX509Cert(self):
624        """Get user cert X509Cert instance"""
625        return self._userX509Cert
626
627    userX509Cert = property(fget=_getUserX509Cert,
628                            fset=_setUserX509Cert,
629                            doc="X.509 user certificate instance")
630 
631    def _setUserX509CertFilePath(self, filePath):
632        "Set user X.509 cert file path property method"
633       
634        if isinstance(filePath, basestring):
635            filePath = os.path.expandvars(filePath)
636            self._userX509Cert = X509CertRead(filePath)
637           
638        elif filePath is not None:
639            raise AttributeError("User X.509 cert. file path must be a valid "
640                                 "string")
641       
642        self._userX509CertFilePath = filePath
643               
644    userX509CertFilePath = property(fset=_setUserX509CertFilePath,
645                                    doc="File path to user X.509 cert.")
646   
647    def _setIssuingX509Cert(self, issuingX509Cert):
648        "Set property method for X.509 user cert."
649        self._issuingX509Cert = self._setX509Cert(issuingX509Cert)
650       
651    def _getIssuingX509Cert(self):
652        """Get user cert X509Cert instance"""
653        return self._issuingX509Cert
654
655    issuingX509Cert = property(fget=_getIssuingX509Cert,
656                               fset=_setIssuingX509Cert,
657                               doc="X.509 user certificate instance")
658 
659    def _setIssuerX509CertFilePath(self, filePath):
660        "Set user X.509 cert file path property method"
661       
662        if isinstance(filePath, basestring):
663            filePath = os.path.expandvars(filePath)
664            self._issuerX509Cert = X509CertRead(filePath)
665           
666        elif filePath is not None:
667            raise AttributeError("User X.509 cert. file path must be a valid "
668                                 "string")
669       
670        self._issuerX509CertFilePath = filePath
671               
672    issuerX509CertFilePath = property(fset=_setIssuerX509CertFilePath,
673                                      doc="File path to user X.509 cert. "
674                                          "issuing cert.")     
675
676    def _getUserPriKey(self):
677        "Get method for user private key"
678        return self._userPriKey
679   
680    def _setUserPriKey(self, userPriKey):
681        """Set method for user private key
682       
683        Nb. if input is a string, userPriKeyPwd will need to be set if
684        the key is password protected.
685       
686        @type userPriKey: M2Crypto.RSA.RSA / string
687        @param userPriKey: private key used to sign message"""
688       
689        if userPriKey is None:
690            self._userPriKey = None
691        elif isinstance(userPriKey, basestring):
692            pwdCallback = lambda *ar, **kw: self._userPriKeyPwd
693            self._userPriKey = RSA.load_key_string(userPriKey,
694                                                   callback=pwdCallback)
695        elif isinstance(userPriKey, RSA.RSA):
696            self._userPriKey = userPriKey         
697        else:
698            raise AttributeError("user private key must be a valid "
699                                 "M2Crypto.RSA.RSA type or a string")
700               
701    userPriKey = property(fget=_getUserPriKey,
702                          fset=_setUserPriKey,
703                          doc="User private key if set, used to sign outbound "
704                              "messages to Attribute authority")
705
706    def _setUserPriKeyFilePath(self, filePath):
707        "Set user private key file path property method"
708       
709        if isinstance(filePath, basestring):
710            filePath = os.path.expandvars(filePath)
711            try:
712                # Read Private key to sign with   
713                priKeyFile = BIO.File(open(filePath)) 
714                pwdCallback = lambda *ar, **kw: self._userPriKeyPwd
715                self._userPriKey = RSA.load_key_bio(priKeyFile, 
716                                                    callback=pwdCallback)   
717            except Exception, e:
718                raise AttributeError("Setting user private key: %s" % e)
719       
720        elif filePath is not None:
721            raise AttributeError("Private key file path must be a valid "
722                                 "string or None")
723       
724        self._userPriKeyFilePath = filePath
725       
726    userPriKeyFilePath = property(fset=_setUserPriKeyFilePath,
727                                  doc="File path to user private key")
728 
729    def _setUserPriKeyPwd(self, userPriKeyPwd):
730        "Set method for user private key file password"
731        if userPriKeyPwd is not None and not isinstance(userPriKeyPwd, 
732                                                        basestring):
733            raise AttributeError("Signing private key password must be None "
734                                 "or a valid string")
735       
736        # Explicitly convert to string as M2Crypto OpenSSL wrapper fails with
737        # unicode type
738        self._userPriKeyPwd = str(userPriKeyPwd)
739
740    def _getUserPriKeyPwd(self):
741        "Get property method for user private key"
742        return self._userPriKeyPwd
743       
744    userPriKeyPwd = property(fset=_setUserPriKeyPwd,
745                             fget=_getUserPriKeyPwd,
746                             doc="Password protecting user private key file")
747       
748    def _getCACertFilePathList(self):
749        """Get CA cert or certs used to validate AC signatures and signatures
750        of peer SOAP messages.
751       
752        @rtype caCertFilePathList: basestring, list or tuple
753        @return caCertFilePathList: file path(s) to CA certificates."""
754        return self._caCertFilePathList
755   
756    def _setCACertFilePathList(self, caCertFilePathList):
757        """Set CA cert or certs to validate AC signatures, signatures
758        of Attribute Authority SOAP responses and SSL connections where
759        AA SOAP service is run over SSL.
760       
761        @type caCertFilePathList: basestring, list, tuple or None
762        @param caCertFilePathList: file path(s) to CA certificates.  If None
763        then the input is quietly ignored."""
764       
765        if isinstance(caCertFilePathList, basestring):
766           self._caCertFilePathList = [caCertFilePathList]
767           
768        elif isinstance(caCertFilePathList, list):
769           self._caCertFilePathList = caCertFilePathList
770           
771        elif isinstance(caCertFilePathList, tuple):
772           self._caCertFilePathList = list(caCertFilePathList)
773
774        elif caCertFilePathList is not None:
775            raise TypeError('Expecting string/list/tuple or None type for '
776                            '"caCertFilePathList"; got %r type' % 
777                            type(caCertFilePathList))     
778       
779    caCertFilePathList = property(fget=_getCACertFilePathList,
780                                  fset=_setCACertFilePathList,
781                                  doc="CA Certificates - used for "
782                                      "verification of AC and SOAP message "
783                                      "signatures")
784           
785    def _setAttributeAuthorityURI(self, attributeAuthorityURI):
786        """Set property method for Attribute Authority Web Service URI to
787        connect to.  This method ALSO RESETS attributeAuthority - a local
788        Attribute Authority instance - to None
789       
790        @type attributeAuthorityURI: basestring/None
791        @param attributeAuthorityURI: Attribute Authority Web Service URI.  Set
792        to None to initialise."""
793        super(CredentialWallet, self)._setAttributeAuthorityURI(
794                                                        attributeAuthorityURI)
795             
796        # Re-initialize local instance
797        self._attributeAuthority = CredentialWallet.propertyDefaults[
798                                                        'attributeAuthority']
799           
800    attributeAuthorityURI = property(
801                            fget=CredentialWalletBase._getAttributeAuthorityURI,
802                            fset=_setAttributeAuthorityURI,
803                            doc="Attribute Authority address - setting also "
804                            "sets up AttributeAuthorityClient instance!")
805
806    def _getAttributeAuthority(self):
807        """Get property method for Attribute Authority Web Service client
808        instance.  Use attributeAuthorityURI propert to set up
809        attributeAuthorityClnt
810       
811        @rtype attributeAuthority: ndg.security.server.attributeauthority.AttributeAuthority
812        @return attributeAuthority: Attribute Authority instance"""
813        return self._attributeAuthority
814
815    def _setAttributeAuthority(self, attributeAuthority):
816        """Set property method for Attribute Authority Web Service instance to
817        connect to.  This method ALSO RESETS attributeAuthorityURI - the
818        address of a remote Attribute Authority - to None
819       
820        @type attributeAuthority: ndg.security.server.attributeauthority.AttributeAuthority
821        @param attributeAuthority: Attribute Authority instance."""
822        if attributeAuthority is not None and \
823           not isinstance(attributeAuthority, AttributeAuthority):
824            raise AttributeError("Expecting %r for attributeAuthority "
825                                 "attribute" % AttributeAuthority)
826           
827        self._attributeAuthority = attributeAuthority
828       
829        # Re-initialize setting for remote service
830        self._attributeAuthorityURI = \
831                    CredentialWallet.propertyDefaults['attributeAuthorityURI']
832           
833    attributeAuthority = property(fget=_getAttributeAuthority,
834                                  fset=_setAttributeAuthority, 
835                                  doc="Attribute Authority instance")
836
837
838    def _getMapFromTrustedHosts(self):
839        """Get property method for boolean flag - if set to True it allows
840        role mapping to be attempted when connecting to an Attribute Authority
841       
842        @type mapFromTrustedHosts: bool
843        @param mapFromTrustedHosts: set to True to try role mapping in AC
844        requests to Attribute Authorities"""
845        return self._mapFromTrustedHosts
846
847    def _setMapFromTrustedHosts(self, mapFromTrustedHosts):
848        """Set property method for boolean flag - if set to True it allows
849        role mapping to be attempted when connecting to an Attribute Authority
850       
851        @type mapFromTrustedHosts: bool
852        @param mapFromTrustedHosts: Attribute Authority Web Service."""
853        if not isinstance(mapFromTrustedHosts, bool):
854            raise AttributeError("Expecting %r for mapFromTrustedHosts "
855                                 "attribute" % bool)
856           
857        self._mapFromTrustedHosts = mapFromTrustedHosts
858           
859    mapFromTrustedHosts = property(fget=_getMapFromTrustedHosts,
860                                   fset=_setMapFromTrustedHosts, 
861                                   doc="Set to True to enable mapped AC "
862                                       "requests")
863
864    def _getRtnExtAttCertList(self):
865        """Get property method for Attribute Authority Web Service client
866        instance.  Use rtnExtAttCertListURI propert to set up
867        rtnExtAttCertListClnt
868       
869        @type rtnExtAttCertList: bool
870        @param rtnExtAttCertList: """
871        return self._rtnExtAttCertList
872
873    def _setRtnExtAttCertList(self, rtnExtAttCertList):
874        """Set property method for boolean flag - when a AC request fails,
875        return a list of candidate ACs that could be used to re-try with in
876        order to get mapped AC.
877       
878        @type rtnExtAttCertList: bool
879        @param rtnExtAttCertList: set to True to configure getAttCert to return
880        a list of ACs that could be used in a re-try to get a mapped AC from
881        the target Attribute Authority."""
882        if not isinstance(rtnExtAttCertList, bool):
883            raise AttributeError("Expecting %r for rtnExtAttCertList "
884                                 "attribute" % bool)
885           
886        self._rtnExtAttCertList = rtnExtAttCertList
887           
888    rtnExtAttCertList = property(fget=_getRtnExtAttCertList,
889                                 fset=_setRtnExtAttCertList, 
890                                 doc="Set to True to enable mapped AC "
891                                     "requests")
892
893    def isValid(self, **x509CertKeys):
894        """Check wallet's user cert.  If expired return False
895       
896        @type **x509CertKeys: dict
897        @param **x509CertKeys: keywords applying to
898        ndg.security.common.X509.X509Cert.isValidTime method"""
899        if self._userX509Cert is not None:
900            return self._userX509Cert.isValidTime(**x509CertKeys)
901        else:
902            log.warning("CredentialWallet.isValid: no user certificate set in "
903                        "wallet")
904            return True
905
906    def addCredential(self, 
907                      attCert, 
908                      attributeAuthorityURI=None,
909                      bUpdateCredentialRepository=True):
910        """Add a new attribute certificate to the list of credentials held.
911
912        @type attCert:
913        @param attCert: new attribute Certificate to be added
914        @type attributeAuthorityURI: basestring
915        @param attributeAuthorityURI: input the Attribute Authority URI from
916        which attCert was retrieved.  This is added to a dict to enable access
917        to a given Attribute Certificate keyed by Attribute Authority URI.
918        See the getCredential method.
919        @type bUpdateCredentialRepository: bool
920        @param bUpdateCredentialRepository: if set to True, and a repository
921        exists it will be updated with the new credentials also
922       
923        @rtype: bool
924        @return: True if certificate was added otherwise False.  - If an
925        existing certificate from the same issuer has a later expiry it will
926        take precedence and the new input certificate is ignored."""
927
928        # Check input
929        if not isinstance(credential, attCert):
930            raise CredentialWalletError("Credential must be an %r type object" %
931                                        AttCert)
932           
933        # Check certificate validity
934        try:
935            attCert.isValid(raiseExcep=True)
936           
937        except AttCertError, e:
938            raise CredentialWalletError("Adding Credential: %s" % e)
939
940        # Check to see if there is an existing Attribute Certificate held
941        # that was issued by the same host.  If so, compare the expiry time.
942        # The one with the latest expiry will be retained and the other
943        # ingored
944        bUpdateCred = True
945        issuerName = attCert['issuerName']
946       
947        if issuerName in self.credentials:
948            # There is an existing certificate held with the same issuing
949            # host name as the new certificate
950            attCertOld = self.credentials[issuerName]['attCert']
951
952            # Get expiry times in datetime format to allow comparison
953            dtAttCertOldNotAfter = attCertOld.getValidityNotAfter(\
954                                                            asDatetime=True)
955            dtAttCertNotAfter = attCert.getValidityNotAfter(asDatetime=True)
956
957            # If the new certificate has an earlier expiry time then ignore it
958            bUpdateCred = dtAttCertNotAfter > dtAttCertOldNotAfter
959
960               
961        if bUpdateCred:
962            # Update: Nb. -1 ID value flags item as new.  Items read in
963            # from the CredentialRepository during creation of the wallet will
964            # have +ve IDs previously allocated by the database
965            self.credentials[issuerName] = {
966                'id': -1, 
967                'attCert': attCert,
968                'issuerName': issuerName,
969                'attributeAuthorityURI': attributeAuthorityURI
970            }
971
972            if attributeAuthorityURI:
973                self.credentialsKeyedByURI[attributeAuthorityURI] = \
974                    self.credentials[issuerName]
975           
976            # Update the Credentials Repository - the permanent store of user
977            # authorisation credentials.  This allows credentials for previous
978            # sessions to be re-instated
979            if self.credentialRepository and bUpdateCredentialRepository:
980                self.updateCredentialRepository()
981
982        # Flag to caller to indicate whether the input certificate was added
983        # to the credentials or an exsiting certificate from the same issuer
984        # took precedence
985        return bUpdateCred
986           
987
988    def audit(self):
989        """Check the credentials held in the wallet removing any that have
990        expired or are otherwise invalid."""
991
992        log.debug("CredentialWallet.audit ...")
993       
994        # Nb. No signature check is carried out.  To do a check, access is
995        # needed to the cert of the CA that issued the Attribute Authority's
996        # cert
997        #
998        # P J Kershaw 12/09/05
999        for key, val in self.credentials.items():
1000            if not val['attCert'].isValid(chkSig=False):
1001                del self.credentials[key]
1002
1003    def updateCredentialRepository(self, auditCred=True):
1004        """Copy over non-persistent credentials held by wallet into the
1005        perminent repository.
1006       
1007        @type auditCred: bool
1008        @param auditCred: filter existing credentials in the repository
1009        removing invalid ones"""
1010
1011        log.debug("CredentialWallet.updateCredentialRepository ...")
1012       
1013        if not self.credentialRepository:
1014            raise CredentialWalletError("No Credential Repository has been "
1015                                        "created for this wallet")
1016                           
1017        # Filter out invalid certs unless auditCred flag is explicitly set to
1018        # false
1019        if auditCred: self.audit()
1020
1021        # Update the database - only add new entries i.e. with an ID of -1
1022        attCertList = [i['attCert'] for i in self.credentials.values() 
1023                       if i['id'] == -1]
1024
1025        self.credentialRepository.addCredentials(self.userId, attCertList)
1026
1027    def _createAttributeAuthorityClnt(self, attributeAuthorityURI):
1028        """Set up a client to an Attribute Authority with the given URI
1029       
1030        @type attributeAuthorityURI: string
1031        @param attributeAuthorityURI: Attribute Authority Web Service URI.
1032
1033        @rtype: ndg.security.common.attributeauthority.AttributeAuthorityClient
1034        @return: new Attribute Authority client instance"""
1035
1036        log.debug('CredentialWallet._createAttributeAuthorityClnt for '
1037                  'service: "%s"' % attributeAuthorityURI)
1038
1039        attributeAuthorityClnt = AttributeAuthorityClient(
1040                            uri=attributeAuthorityURI,
1041                            sslCACertFilePathList=self.sslCACertFilePathList,
1042                            cfg=self.wssCfgFilePath or self._cfg,
1043                            cfgFileSection=self.wssCfgSection,
1044                            cfgFilePrefix=self.wssCfgPrefix,
1045                            **(self.wssCfgKw or {}))
1046       
1047        # If a user certificate is set, use this to sign messages instead of
1048        # the default settings in the WS-Security config. 
1049        if attributeAuthorityClnt.signatureHandler is not None and \
1050           self.userPriKey is not None:
1051            if self.issuingX509Cert is not None:
1052                # Pass a chain of certificates -
1053                # Initialise WS-Security signature handling to pass
1054                # BinarySecurityToken containing user cert and cert for user
1055                # cert issuer
1056                attributeAuthorityClnt.signatureHandler.reqBinSecTokValType = \
1057                            SignatureHandler.binSecTokValType["X509PKIPathv1"]
1058                attributeAuthorityClnt.signatureHandler.signingCertChain = (
1059                                    self.issuingX509Cert, self.userX509Cert)               
1060
1061                attributeAuthorityClnt.signatureHandler.signingPriKey = \
1062                                                            self.userPriKey
1063            elif self.userX509Cert is not None:
1064                # Pass user cert only - no need to pass a cert chain. 
1065                # This type of token is more likely to be supported by the
1066                # various WS-Security toolkits
1067                attributeAuthorityClnt.signatureHandler.reqBinSecTokValType = \
1068                                    SignatureHandler.binSecTokValType["X509v3"]
1069                attributeAuthorityClnt.signatureHandler.signingCert = \
1070                                                            self.userX509Cert
1071
1072                attributeAuthorityClnt.signatureHandler.signingPriKey = \
1073                                                            self.userPriKey
1074
1075        return attributeAuthorityClnt
1076
1077
1078    def _getAttCert(self, 
1079                    attributeAuthorityURI=None, 
1080                    attributeAuthority=None,
1081                    extAttCert=None):       
1082        """Wrapper to Attribute Authority attribute certificate request.  See
1083        getAttCert for the classes' public interface.
1084       
1085        If successful, a new attribute certificate is issued to the user
1086        and added into the wallet
1087       
1088        @type attributeAuthorityURI: string
1089        @param attributeAuthorityURI: to call as a web service, specify the URI
1090        for the Attribute Authority.
1091       
1092        @type attributeAuthority: ndg.security.server.attributeauthority.AttributeAuthority
1093        @param attributeAuthority: Alternative to attributeAuthorityURI - to
1094        run on the local machine, specify a local Attribute Authority
1095        instance.
1096
1097        @type extAttCert: ndg.security.common.AttCert.AttCert
1098        @param extAttCert: an existing Attribute Certificate which can
1099        be used to making a mapping should the user not be registered with the
1100        Attribute Authority"""
1101     
1102        log.debug("CredentialWallet._getAttCert ...")
1103       
1104       
1105        # If a user cert. is present, ignore the user ID setting.  The
1106        # Attribute Authority will set the userId field of the
1107        # Attribute Certificate based on the DN of the user certificate
1108        if self.userX509Cert:
1109            userId = str(self.userX509Cert.dn)
1110        else:
1111            userId = self.userId
1112           
1113        if attributeAuthority is not None and \
1114           attributeAuthorityURI is not None:
1115            raise KeyError("Both attributeAuthorityURI and attributeAuthority "
1116                           "keywords have been set")
1117       
1118        if attributeAuthority is None:
1119            attributeAuthority = self.attributeAuthority
1120           
1121        if attributeAuthorityURI is None:
1122            attributeAuthorityURI = self.attributeAuthorityURI
1123           
1124        # Set a client alias according to whether the Attribute Authority is
1125        # being called locally or as a remote service
1126        if attributeAuthorityURI is not None:
1127            # Call Remote Service at given URI
1128            aaInterface = self._createAttributeAuthorityClnt(
1129                                                        attributeAuthorityURI)                           
1130            log.debug('CredentialWallet._getAttCert for remote Attribute '
1131                      'Authority service: "%s" ...' % attributeAuthorityURI)
1132               
1133        elif attributeAuthority is not None:
1134            # Call local based Attribute Authority with settings from the
1135            # configuration file attributeAuthority
1136            aaInterface = attributeAuthority
1137            log.debug('CredentialWallet._getAttCert for local Attribute '
1138                      'Authority: "%r" ...' % attributeAuthority)
1139        else:
1140            raise CredentialWalletError("Error requesting attribute: "
1141                                        "certificate a URI or Attribute "
1142                                        "Authority instance must be specified")
1143       
1144        try:
1145            # Request a new attribute certificate from the Attribute
1146            # Authority
1147            attCert = aaInterface.getAttCert(userId=userId,
1148                                             userAttCert=extAttCert)
1149           
1150            log.info('Granted Attribute Certificate from issuer DN = "%s"'%
1151                     attCert.issuerDN)
1152           
1153        except (AttributeAuthorityAccessDenied, AttributeRequestDenied), e:
1154            # AttributeAuthorityAccessDenied is raised if
1155            # aaInterface is a local AA instance and
1156            # AttributeRequestDenied is raised for a client to a remote AA
1157            # service
1158            raise CredentialWalletAttributeRequestDenied(str(e))
1159                   
1160        except Exception, e:
1161            raise CredentialWalletError("Requesting attribute certificate: %s"%
1162                                        e)
1163
1164        # Update attribute Certificate instance with CA's certificate ready
1165        # for signature check in addCredential()
1166        if self._caCertFilePathList is None:
1167            raise CredentialWalletError("No CA certificate has been set")
1168       
1169        attCert.certFilePathList = self._caCertFilePathList
1170
1171       
1172        # Add credential into wallet
1173        #
1174        # Nb. if the certificates signature is invalid, it will be rejected
1175        log.debug("Adding credentials into wallet...")
1176        self.addCredential(attCert)
1177       
1178        return attCert
1179
1180    def _getAAHostInfo(self, 
1181                       attributeAuthority=None,
1182                       attributeAuthorityURI=None):
1183        """Wrapper to Attribute Authority getHostInfo
1184       
1185        _getAAHostInfo([attributeAuthority=f|attributeAuthorityURI=u])
1186                   
1187        @type userRole: string
1188        @param userRole: get hosts which have a mapping to this role
1189       
1190        @type attributeAuthorityURI: string
1191        @param attributeAuthorityURI: to call as a web service, specify the URI
1192        for the Attribute Authority.
1193       
1194        @type attributeAuthority: string
1195        @param attributeAuthority: Alternative to attributeAuthorityURI - to
1196        run on the local machine, specify the local Attribute Authority
1197        instance.
1198        """
1199
1200        if attributeAuthority is None:
1201            attributeAuthority = self.attributeAuthority
1202           
1203        if attributeAuthorityURI is None:
1204            attributeAuthorityURI = self.attributeAuthorityURI
1205       
1206        log.debug('CredentialWallet._getAAHostInfo for service: "%s" ...' % 
1207                  attributeAuthorityURI or attributeAuthority)
1208           
1209        # Set a client alias according to whether the Attribute Authority is
1210        # being called locally or asa remote service
1211        if attributeAuthorityURI is not None:
1212            # Call Remote Service at given URI
1213            attributeAuthorityClnt = self._createAttributeAuthorityClnt(
1214                                                    attributeAuthorityURI)
1215
1216        elif attributeAuthority is not None:
1217            # Call local based Attribute Authority with settings from the
1218            # configuration file attributeAuthority
1219            attributeAuthorityClnt = attributeAuthority
1220           
1221        else:
1222            raise CredentialWalletError("Error requesting trusted hosts info: " 
1223                                        "a URI or Attribute Authority " 
1224                                        "configuration file must be specified")
1225           
1226        try:
1227            # Request a new attribute certificate from the Attribute
1228            # Authority
1229            return attributeAuthorityClnt.getHostInfo()
1230           
1231        except Exception, e:
1232            log.error("Requesting host info: %s" % e)
1233            raise
1234
1235    def _getAATrustedHostInfo(self, 
1236                              userRole=None,
1237                              attributeAuthority=None,
1238                              attributeAuthorityURI=None):
1239        """Wrapper to Attribute Authority getTrustedHostInfo
1240       
1241        _getAATrustedHostInfo([userRole=r, ][attributeAuthority=f|
1242                              attributeAuthorityURI=u])
1243                   
1244        @type userRole: string
1245        @param userRole: get hosts which have a mapping to this role
1246       
1247        @type attributeAuthorityURI: string
1248        @param attributeAuthorityURI: to call as a web service, specify the URI
1249        for the Attribute Authority.
1250       
1251        @type attributeAuthority: string
1252        @param attributeAuthority: Alternative to attributeAuthorityURI - to
1253        run on the local machine, specify the local Attribute Authority
1254        instance.
1255        """
1256
1257        if attributeAuthority is None:
1258            attributeAuthority = self.attributeAuthority
1259           
1260        if attributeAuthorityURI is None:
1261            attributeAuthorityURI = self.attributeAuthorityURI
1262       
1263        log.debug('CredentialWallet._getAATrustedHostInfo for role "%s" and '
1264                  'service: "%s" ...' % (userRole, 
1265                                attributeAuthorityURI or attributeAuthority))
1266           
1267        # Set a client alias according to whether the Attribute Authority is
1268        # being called locally or asa remote service
1269        if attributeAuthorityURI is not None:
1270            # Call Remote Service at given URI
1271            attributeAuthorityClnt = self._createAttributeAuthorityClnt(
1272                                                    attributeAuthorityURI)
1273
1274        elif attributeAuthority is not None:
1275            # Call local based Attribute Authority with settings from the
1276            # configuration file attributeAuthority
1277            attributeAuthorityClnt = attributeAuthority
1278           
1279        else:
1280            raise CredentialWalletError("Error requesting trusted hosts info: " 
1281                                        "a URI or Attribute Authority " 
1282                                        "configuration file must be specified")
1283           
1284        try:
1285            # Request a new attribute certificate from the Attribute
1286            # Authority
1287            return attributeAuthorityClnt.getTrustedHostInfo(role=userRole)
1288           
1289        except Exception, e:
1290            log.error("Requesting trusted host info: %s" % e)
1291            raise
1292
1293    def getAttCert(self,
1294                   reqRole=None,
1295                   attributeAuthority=None,
1296                   attributeAuthorityURI=None,
1297                   mapFromTrustedHosts=None,
1298                   rtnExtAttCertList=None,
1299                   extAttCertList=None,
1300                   extTrustedHostList=None,
1301                   refreshAttCert=False,
1302                   attCertRefreshElapse=None):
1303       
1304        """Get an Attribute Certificate from an Attribute Authority.  If this
1305        fails try to make a mapped Attribute Certificate by using a certificate
1306        from another host which has a trust relationship to the Attribute
1307        Authority in question.
1308
1309        getAttCert([reqRole=r, ][attributeAuthority=a|attributeAuthorityURI=u,]
1310                   [mapFromTrustedHosts=m, ]
1311                   [rtnExtAttCertList=e, ][extAttCertList=el, ]
1312                   [extTrustedHostList=et, ][refreshAttCert=ra])
1313                 
1314        The procedure is:
1315
1316        1) Try attribute request using user certificate
1317        2) If the Attribute Authority (AA) doesn't recognise the certificate,
1318        find out any other hosts which have a trust relationship to the AA.
1319        3) Look for Attribute Certificates held in the wallet corresponding
1320        to these hosts.
1321        4) If no Attribute Certificates are available, call the relevant
1322        hosts' AAs to get certificates
1323        5) Finally, use these new certificates to try to obtain a mapped
1324        certificate from the original AA
1325        6) If this fails access is denied     
1326                   
1327        @type reqRole: string
1328        @param reqRole: the required role to get access for
1329       
1330        @type attributeAuthorityURI: string
1331        @param attributeAuthorityURI: to call as a web service, specify the URI
1332        for the Attribute Authority.
1333       
1334        @type attributeAuthority: string
1335        @param attributeAuthority: Altenrative to attributeAuthorityURI - to
1336        run on the local machine, specify a local Attribute Authority
1337        instance.
1338                               
1339        @type mapFromTrustedHosts: bool / None     
1340        @param mapFromTrustedHosts: if request fails via the user's cert
1341        ID, then it is possible to get a mapped certificate by using
1342        certificates from other AA's.  Set this flag to True, to allow this
1343        second stage of generating a mapped certificate from the certificate
1344        stored in the wallet credentials.
1345
1346        If set to False, it is possible to return the list of certificates
1347        available for mapping and then choose which one or ones to use for
1348        mapping by re-calling getAttCert with extAttCertList set to these
1349        certificates.
1350       
1351        Defaults to None in which case self._mapFromTrustedHosts is not
1352        altered
1353
1354        The list is returned via CredentialWalletAttributeRequestDenied
1355        exception.  If no value is set, the default value held in
1356        self.mapFromTrustedHosts is used
1357
1358        @type rtnExtAttCertList: bool / None
1359        @param rtnExtAttCertList: If request fails, make a list of
1360        candidate certificates from other Attribute Authorities which the user
1361        could use to retry and get a mapped certificate.
1362                               
1363        If mapFromTrustedHosts is set True this flags value is overriden and
1364        effectively set to True.
1365
1366        If no value is set, the default value held in self._rtnExtAttCertList
1367        is used.
1368                               
1369        The list is returned via a CredentialWalletAttributeRequestDenied
1370        exception object.
1371                               
1372        @type extAttCertList: list
1373        @param extAttCertList: Attribute Certificate or list of certificates
1374        from other Attribute Authorities.  These can be used to get a mapped
1375        certificate if access fails based on the user's certificate
1376        credentials.  They are tried out in turn until access is granted so
1377        the order of the list decides the order in which they will be tried
1378
1379        @type extTrustedHostList:
1380        @param extTrustedHostList: same as extAttCertList keyword, but
1381        instead of providing Attribute Certificates, give a list of Attribute
1382        Authority hosts.  These will be matched up to Attribute Certificates
1383        held in the wallet.  Matching certificates will then be used to try to
1384        get a mapped Attribute Certificate.
1385       
1386        @type refreshAttCert: bool
1387        @param refreshAttCert: if set to True, the attribute request
1388        will go ahead even if the wallet already contains an Attribute
1389        Certificate from the target Attribute Authority.  The existing AC in
1390        the wallet will be replaced by the new one obtained from this call.
1391                               
1392        If set to False, this method will check to see if an AC issued by the
1393        target AA already exists in the wallet.  If so, it will return this AC
1394        to the caller without proceeding to make a call to the AA.
1395       
1396        @type attCertRefreshElapse: float / int
1397        @param attCertRefreshElapse: determine whether to replace an
1398        existing AC in the cache with a fresh one.  If the existing one has
1399        less than attCertRefreshElapse time in seconds left before expiry then
1400        replace it.
1401       
1402        @rtype: ndg.security.common.AttCert.AttCert
1403        @return: Attribute Certificate retrieved from Attribute Authority"""
1404       
1405        log.debug("CredentialWallet.getAttCert ...")
1406       
1407        # Both these assignments are calling set property methods implicitly!
1408        if attributeAuthorityURI:
1409            self.attributeAuthorityURI = attributeAuthorityURI
1410           
1411        if attributeAuthority is not None:
1412            self.attributeAuthority = attributeAuthority
1413           
1414        if not refreshAttCert and self.credentials:
1415            # Refresh flag is not set so it's OK to check for any existing
1416            # Attribute Certificate in the wallet whose issuerName match the
1417            # target AA's name
1418           
1419            # Find out the site ID for the target AA by calling AA's host
1420            # info WS method
1421            log.debug("CredentialWallet.getAttCert - check AA site ID ...")
1422            try:
1423                hostInfo = self._getAAHostInfo()
1424                aaName = hostInfo.keys()[0]
1425            except Exception, e:
1426                raise CredentialWalletError("Getting host info: %s" % e)
1427           
1428            # Look in the wallet for an AC with the same issuer name
1429            if aaName in self.credentials:
1430                # Existing Attribute Certificate found in wallet - Check that
1431                # it will be valid for at least the next 2 hours
1432                if attCertRefreshElapse is not None:
1433                    self.attCertRefreshElapse = attCertRefreshElapse
1434                   
1435                dtNow = datetime.utcnow() + \
1436                        timedelta(seconds=self.attCertRefreshElapse)
1437               
1438                attCert = self.credentials[aaName]['attCert']
1439                if attCert.isValidTime(dtNow=dtNow):
1440                    log.info("Retrieved an existing %s AC from the wallet" % 
1441                             aaName)
1442                    return attCert
1443                     
1444        # Check for settings from input, if not set use previous settings
1445        # made
1446        if mapFromTrustedHosts is not None:
1447            self.mapFromTrustedHosts = mapFromTrustedHosts
1448
1449        if rtnExtAttCertList is not None:
1450            self.rtnExtAttCertList = rtnExtAttCertList
1451
1452        # Check for list of external trusted hosts (other trusted NDG data
1453        # centres)
1454        if extTrustedHostList:
1455            log.info("Checking for ACs in wallet matching list of trusted "
1456                     "hosts set: %s" % extTrustedHostList)
1457           
1458            if not self.mapFromTrustedHosts:
1459                raise CredentialWalletError("A list of trusted hosts has been " 
1460                                      "input but mapping from trusted hosts "
1461                                      "is set to disallowed")
1462           
1463            if isinstance(extTrustedHostList, basestring):
1464                extTrustedHostList = [extTrustedHostList]
1465
1466            # Nb. Any extAttCertList is overriden by extTrustedHostList being
1467            # set
1468            extAttCertList = [self.credentials[hostName]['attCert'] 
1469                              for hostName in extTrustedHostList
1470                              if hostName in self.credentials]
1471
1472        # Set an empty list to trigger an AttributeError by initialising it to
1473        # None
1474        if extAttCertList == []:
1475            extAttCertList = None
1476           
1477        # Repeat authorisation attempts until succeed or means are exhausted
1478        while True:
1479           
1480            # Check for candidate certificates for mapping
1481            try:
1482                # If list is set get the next cert
1483                extAttCert = extAttCertList.pop()
1484
1485            except AttributeError:
1486                log.debug("No external Attribute Certificates - trying "
1487                          "request without mapping...")
1488                # No List set - attempt request without
1489                # using mapping from trusted hosts
1490                extAttCert = None
1491                           
1492            except IndexError:
1493               
1494                # List has been emptied without attribute request succeeding -
1495                # give up
1496                errMsg = ("Attempting to obtained a mapped certificate: "
1497                          "no external attribute certificates are available")
1498                   
1499                # Add the exception form the last call to the Attribute
1500                # Authority if an error exists
1501                try:
1502                    errMsg += ": %s" % attributeRequestDenied
1503                except NameError:
1504                    pass
1505
1506                raise CredentialWalletAttributeRequestDenied(errMsg)
1507                                                   
1508               
1509            # Request Attribute Certificate from Attribute Authority
1510            try:
1511                attCert = self._getAttCert(extAttCert=extAttCert)               
1512                # Access granted
1513                return attCert
1514           
1515            except CredentialWalletAttributeRequestDenied, \
1516                   attributeRequestDenied:
1517                if not self.mapFromTrustedHosts and not self.rtnExtAttCertList:
1518                    log.debug("Creating a mapped certificate option is not "
1519                              "set - raising "
1520                              "CredentialWalletAttributeRequestDenied "
1521                              "exception saved from earlier")
1522                    raise attributeRequestDenied
1523
1524                if isinstance(extAttCertList, list):
1525                    # An list of attribute certificates from trusted hosts
1526                    # is present continue cycling through this until one of
1527                    # them is accepted and a mapped certificate can be derived
1528                    log.debug("AC request denied - but external ACs available "
1529                              "to try mapped AC request ...")
1530                    continue
1531                             
1532                #  Use the input required role and the AA's trusted host list
1533                # to identify attribute certificates from other hosts which
1534                # could be used to make a mapped certificate
1535                log.debug("Getting a list of trusted hosts for mapped AC "
1536                          "request ...")
1537                try:
1538                    trustedHostInfo = self._getAATrustedHostInfo(reqRole)
1539                   
1540                except NoMatchingRoleInTrustedHosts, e:
1541                    raise CredentialWalletAttributeRequestDenied(
1542                        'Can\'t get a mapped Attribute Certificate for '
1543                        'the "%s" role' % reqRole)
1544               
1545                except Exception, e:
1546                    raise CredentialWalletError("Getting trusted hosts: %s"%e)
1547
1548                if not trustedHostInfo:
1549                    raise CredentialWalletAttributeRequestDenied(
1550                        "Attribute Authority has no trusted hosts with "
1551                        "which to make a mapping")
1552
1553                # Initialise external certificate list here - if none are
1554                # found IndexError will be raised on the next iteration and
1555                # an access denied error will be raised
1556                extAttCertList = []
1557
1558                # Look for Attribute Certificates with matching issuer host
1559                # names
1560                log.debug("Checking wallet for ACs issued by one of the "
1561                          "trusted hosts...")
1562                for hostName in self.credentials:
1563
1564                    # Nb. Candidate certificates for mappings must have
1565                    # original provenance and contain at least one of the
1566                    # required roles
1567                    attCert = self.credentials[hostName]['attCert']
1568                   
1569                    if hostName in trustedHostInfo and attCert.isOriginal():                       
1570                        for role in attCert.roles:
1571                            if role in trustedHostInfo[hostName]['role']:                               
1572                                extAttCertList.append(attCert)
1573
1574                if not extAttCertList:
1575                    log.debug("No wallet ACs matched any of the trusted "
1576                              "hosts.  - Try request for an AC from a "
1577                              "trusted host ...")
1578                   
1579                    # No certificates in the wallet matched the trusted host
1580                    # and required roles
1581                    #
1582                    # Try each host in turn in order to get a certificate with
1583                    # the required credentials in order to do a mapping
1584                    for host, info in trustedHostInfo.items():
1585                        try:
1586                            # Try request to trusted host
1587                            extAttCert = self._getAttCert(
1588                                        attributeAuthorityURI=info['aaURI'])
1589
1590                            # Check the certificate contains at least one of
1591                            # the required roles
1592                            if [True for r in extAttCert.roles
1593                                if r in info['role']]:
1594                               extAttCertList.append(extAttCert)
1595
1596                               # For efficiency, stop once obtained a valid
1597                               # cert - but may want complete list for user to
1598                               # choose from
1599                               #break
1600                               
1601                        except Exception, e:
1602                            # ignore any errors and continue
1603                            log.warning('AC request to trusted host "%s"' 
1604                                        ' resulted in: %s' % (info['aaURI'],e))
1605                           
1606                if not extAttCertList:                       
1607                    raise CredentialWalletAttributeRequestDenied(
1608                        "No certificates are available with which to "
1609                        "make a mapping to the Attribute Authority")
1610
1611
1612                if not self.mapFromTrustedHosts:
1613                   
1614                    # Exit here returning the list of candidate certificates
1615                    # that could be used to make a mapped certificate
1616                    msg = "User is not registered with Attribute " + \
1617                          "Authority - retry using one of the returned " + \
1618                          "Attribute Certificates obtained from other " + \
1619                          "trusted hosts"
1620                         
1621                    raise CredentialWalletAttributeRequestDenied(msg,
1622                                            extAttCertList=extAttCertList,
1623                                            trustedHostInfo=trustedHostInfo)           
1624
1625"""Alias to CredentialWallet to make distinction between this and the SAML
1626implementation"""
1627NDGCredentialWallet = CredentialWallet
1628
1629
1630class SamlCredentialWalletAttributeQueryResponseError(CredentialWalletError):
1631    """Attribute Authority returned a SAML Response error code"""
1632    def __init__(self, *arg, **kw):
1633        SamlCredentialWalletError.__init__(self, *arg, **kw)
1634        self.__status = Status()
1635        self.__status.statusCode = StatusCode()
1636        self.__status.statusMessage = StatusMessage()
1637   
1638    def _getStatus(self):
1639        '''Gets the Status of this response.
1640       
1641        @return the Status of this response
1642        '''
1643        return self.__status
1644
1645    def _setStatus(self, value):
1646        '''Sets the Status of this response.
1647       
1648        @param value: the Status of this response
1649        '''
1650        if not isinstance(value, Status):
1651            raise TypeError('"status" must be a %r, got %r' % (Status,
1652                                                               type(value)))
1653        self.__status = value
1654       
1655    status = property(fget=_getStatus, fset=_setStatus, 
1656                      doc="Attribute Authority SAML Response error status")
1657   
1658    def __str__(self):
1659        if self.status is not None:
1660            return self.status.statusMessage.value or ''
1661        else:
1662            return ''
1663       
1664from uuid import uuid4
1665try: # >= python 2.5
1666    from xml.etree import ElementTree
1667except ImportError:
1668    import ElementTree
1669 
1670import saml   
1671from saml.utils import SAMLDateTime
1672from saml.common.xml import SAMLConstants
1673from saml.saml2.core import (Attribute,
1674                             Assertion, 
1675                             SAMLVersion, 
1676                             Subject, 
1677                             NameID, 
1678                             Issuer, 
1679                             AttributeQuery, 
1680                             XSStringAttributeValue, 
1681                             Status,
1682                             StatusCode,
1683                             StatusMessage)
1684from saml.xml.etree import AssertionElementTree, ResponseElementTree
1685   
1686from ndg.security.common.saml.bindings import SOAPBinding as SamlSoapBinding
1687from ndg.security.common.saml.esg import EsgSamlNamespaces
1688from ndg.security.common.X509 import X500DN
1689from ndg.security.common.utils.m2crypto import SSLContextProxy
1690from ndg.security.common.utils.etree import prettyPrint
1691from ndg.security.common.utils.configfileparsers import (     
1692                                                    CaseSensitiveConfigParser,)
1693
1694
1695class SamlCredentialWallet(CredentialWalletBase, SSLContextProxy):
1696    """CredentialWallet for Earth System Grid supporting SAML based Attribute
1697    Queries
1698   
1699    @cvar DEFAULT_ATTR_DESCR: default attribute to query"""
1700   
1701    XSSTRING_NS = "%s#%s" % (
1702        SAMLConstants.XSD_NS,
1703        XSStringAttributeValue.TYPE_LOCAL_NAME
1704    )
1705    N_ATTR_DESCR_ELEM_ITEMS = 3
1706   
1707    DEFAULT_ATTR_DESCR = (
1708        (EsgSamlNamespaces.FIRSTNAME_ATTRNAME, 
1709         EsgSamlNamespaces.FIRSTNAME_FRIENDLYNAME, 
1710         XSSTRING_NS),
1711        (EsgSamlNamespaces.LASTNAME_ATTRNAME, 
1712         EsgSamlNamespaces.LASTNAME_FRIENDLYNAME, 
1713         XSSTRING_NS),
1714        (EsgSamlNamespaces.EMAILADDRESS_ATTRNAME, 
1715         EsgSamlNamespaces.EMAILADDRESS_FRIENDLYNAME, 
1716         XSSTRING_NS),
1717    )
1718    ESG_NAME_ID_FORMAT = EsgSamlNamespaces.NAMEID_FORMAT
1719   
1720    ATTRIBUTE_AUTHORITY_URI_OPTNAME = 'attributeAuthorityURI'
1721    ISSUER_DN_OPTNAME = 'issuerDN'
1722    CLOCK_SKEW_OPTNAME = 'clockSkew'
1723   
1724    CONFIG_FILE_OPTNAMES = (
1725        ATTRIBUTE_AUTHORITY_URI_OPTNAME,
1726        ISSUER_DN_OPTNAME,                 
1727        CLOCK_SKEW_OPTNAME           
1728    )
1729    __slots__ = (
1730       'attributeDescr'
1731    )
1732    __slots__ += CONFIG_FILE_OPTNAMES
1733    __PRIVATE_ATTR_PREFIX = '_SamlCredentialWallet__'
1734    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__])
1735    del i
1736   
1737    CREDENTIAL_REPOSITORY_NOT_SUPPORTED_MSG = ("SamlCredentialWallet doesn't "
1738                                               "support the CredentialRepository"
1739                                               " interface")
1740    def __init__(self):
1741        super(SamlCredentialWallet, self).__init__()
1742       
1743        self.__issuerDN = None
1744        self.__clockSkew = timedelta(seconds=0.)
1745               
1746        # Use property here as a safeguard in case DEFAULT_ATTR_DESCR has been
1747        # altered
1748        self.attributeDescr = SamlCredentialWallet.DEFAULT_ATTR_DESCR
1749
1750    @classmethod
1751    def fromConfig(cls, cfg, **kw):
1752        '''Alternative constructor makes object from config file settings
1753        @type cfg: basestring /ConfigParser derived type
1754        @param cfg: configuration file path or ConfigParser type object
1755        @rtype: ndg.security.common.credentialWallet.SamlCredentialWallet
1756        @return: new instance of this class
1757        '''
1758        credentialWallet = cls()
1759        credentialWallet.parseConfig(cfg, **kw)
1760       
1761        return credentialWallet
1762
1763    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
1764        '''Read config file settings
1765        @type cfg: basestring /ConfigParser derived type
1766        @param cfg: configuration file path or ConfigParser type object
1767        @type prefix: basestring
1768        @param prefix: prefix for option names e.g. "certExtApp."
1769        @type section: baestring
1770        @param section: configuration file section from which to extract
1771        parameters.
1772        '''
1773       
1774        if isinstance(cfg, basestring):
1775            cfgFilePath = os.path.expandvars(cfg)
1776            _cfg = CaseSensitiveConfigParser()
1777            _cfg.read(configFilePath)
1778           
1779        elif isinstance(cfg, ConfigParser):
1780            _cfg = cfg   
1781        else:
1782            raise AttributeError('Expecting basestring or ConfigParser type '
1783                                 'for "cfg" attribute; got %r type' % type(cfg))     
1784       
1785        for optName in SamlCredentialWallet.CONFIG_FILE_OPTNAMES:
1786            val = _cfg.get(section, prefix+optName)
1787            setattr(self, optName, val)
1788           
1789    def _getAttributeDescr(self):
1790        return self.__attributeDescr
1791
1792    def _setAttributeDescr(self, value):
1793        if not isinstance(value, tuple):
1794            raise TypeError('Expecting tuple type for "attributeDescr";'
1795                            ' got %r instead' % type(value))
1796           
1797        for i in value:
1798            if not isinstance(value, tuple):
1799                raise TypeError('Expecting tuple type for "attributeDescr" '
1800                                'tuple sub-elements; got %r instead' % 
1801                                type(value))
1802            if len(i) != SamlCredentialWallet.N_ATTR_DESCR_ELEM_ITEMS:
1803                raise TypeError('Expecting %d element tuple for '
1804                                '"attributeDescr" sub-elements; got %d '
1805                                'elements instead' % 
1806                                (SamlCredentialWallet.N_ATTR_DESCR_ELEM_ITEMS,
1807                                 len(i)))
1808               
1809        self.__attributeDescr = value
1810   
1811    attributeDescr = property(_getAttributeDescr, 
1812                              _setAttributeDescr, 
1813                              doc="List of name, friendly name, format tuples "
1814                                  "determining attributes to query from the "
1815                                  "Attribute Authority")
1816
1817    def _getIssuerDN(self):
1818        return self.__issuerDN
1819
1820    def _setIssuerDN(self, value):
1821        if isinstance(value, basestring):
1822            self.__issuerDN = X500DN.fromString(value)
1823           
1824        elif isinstance(value, X500DN):
1825            self.__issuerDN = value
1826        else:
1827            raise TypeError('Expecting string or X500DN type for "issuerDN"; '
1828                            'got %r instead' % type(value))
1829        self.__issuerDN = value
1830
1831    issuerDN = property(_getIssuerDN, _setIssuerDN, 
1832                        doc="Distinguished Name of issuer of SAML Attribute "
1833                            "Query to Attribute Authority")
1834
1835    def _getClockSkew(self):
1836        return self.__clockSkew
1837
1838    def _setClockSkew(self, value):
1839        if isinstance(value, (float, int, long)):
1840            self.__clockSkew = timedelta(seconds=value)
1841           
1842        elif isinstance(value, basestring):
1843            self.__clockSkew = timedelta(seconds=float(value))
1844        else:
1845            raise TypeError('Expecting float, int, long or string type for '
1846                            '"clockSkew"; got %r' % type(value))
1847
1848    clockSkew = property(fget=_getClockSkew, 
1849                         fset=_setClockSkew, 
1850                         doc="Allow a clock skew in seconds for SAML Attribute"
1851                             " Query issueInstant parameter check")
1852       
1853    def __getstate__(self):
1854        '''Specific implementation needed with __slots__'''
1855        return dict([(attrName, getattr(self, attrName)) 
1856                     for attrName in SamlCredentialWallet.__slots__])
1857       
1858    def __setstate__(self, attrDict):
1859        '''Specific implementation needed with __slots__'''
1860        for attr, val in attrDict.items():
1861            setattr(self, attr, val)
1862   
1863    @staticmethod
1864    def serialiseAssertion(assertion):
1865        """Convert SAML assertion object into a string"""
1866        samlAssertionElem = AssertionElementTree.toXML(assertion)
1867        return ElementTree.tostring(samlAssertionElem)
1868       
1869    def attributeQuery(self):
1870        """Query an Attribute Authority to retrieve an assertion for the
1871        given user"""
1872               
1873        # Create a SAML attribute query
1874        attributeQuery = AttributeQuery()
1875        attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
1876        attributeQuery.id = str(uuid4())
1877        attributeQuery.issueInstant = datetime.utcnow()
1878       
1879        attributeQuery.issuer = Issuer()
1880        attributeQuery.issuer.format = Issuer.X509_SUBJECT
1881        attributeQuery.issuer.value = self.issuerDN
1882                       
1883        attributeQuery.subject = Subject() 
1884        attributeQuery.subject.nameID = NameID()
1885        attributeQuery.subject.nameID.format = \
1886                                        SamlCredentialWallet.ESG_NAME_ID_FORMAT
1887        attributeQuery.subject.nameID.value = self.userId
1888                 
1889        # Add list of attributes to query                     
1890        for name, friendlyName, format in self.attributeDescr:
1891            attribute = Attribute()
1892            attribute.name = name
1893            attribute.nameFormat = format
1894            attribute.friendlyName = friendlyName
1895   
1896            attributeQuery.attributes.append(attribute)
1897
1898        # Make query over SOAP interface to remote service
1899        binding = SamlSoapBinding()
1900        response = binding.attributeQuery(attributeQuery, 
1901                                          self.attributeAuthorityURI)
1902       
1903        if log.level <= logging.DEBUG:           
1904            log.debug("Attribute Authority [%s] SAML Response:", 
1905                      self.attributeAuthorityURI)
1906            log.debug("_"*80)
1907            responseElem = ResponseElementTree.toXML(response)
1908            log.debug(prettyPrint(responseElem))
1909       
1910        if response.status.statusCode.value != StatusCode.SUCCESS_URI:
1911            samlRespError = SamlCredentialWalletAttributeQueryResponseError()
1912            samlRespError.status = response.status
1913            raise samlRespError
1914       
1915        # Check Query ID matches the query ID the service received
1916        if response.inResponseTo != attributeQuery.id:
1917            samlRespError = SamlCredentialWalletAttributeQueryResponseError()
1918            samlRespError.status = response.status
1919            raise samlRespError
1920       
1921        utcNow = datetime.utcnow() + self.clockSkew
1922        if response.issueInstant > utcNow:
1923            samlRespError = SamlCredentialWalletAttributeQueryResponseError()
1924           
1925            msg = ('SAML Attribute Response issueInstant [%s] is after '
1926                   'the current clock time [%s]' % 
1927                   (attributeQuery.issueInstant, SAMLDateTime.toString(utcNow)))
1928                     
1929            samlRespError.status.statusCode.value = StatusCode.RESPONDER_URI
1930            samlRespError.status.statusMessage.value = msg
1931            raise samlRespError
1932       
1933        if utcNow < response.assertions[-1].conditions.notBefore:
1934            samlRespError = SamlCredentialWalletAttributeQueryResponseError()
1935           
1936            msg = ('The current clock time [%s] is before the SAML Attribute '
1937                   'Response assertion conditions not before time [%s]' % 
1938                   (SAMLDateTime.toString(utcNow),
1939                    response.assertions[-1].conditions.notBefore))
1940                     
1941            samlRespError.status.statusCode.value = StatusCode.RESPONDER_URI
1942            samlRespError.status.statusMessage.value = msg
1943            raise samlRespError
1944         
1945        if utcNow >= response.assertions[-1].conditions.notOnOrAfter:
1946            samlRespError = SamlCredentialWalletAttributeQueryResponseError()       
1947           
1948            msg = ('The current clock time [%s] is on or after the SAML '
1949                   'Attribute Response assertion conditions not on or after '
1950                   'time [%s]' % 
1951                   (SAMLDateTime.toString(utcNow),
1952                    response.assertions[-1].conditions.notOnOrAfter))
1953                     
1954            samlRespError.status.statusCode.value = StatusCode.RESPONDER_URI
1955            samlRespError.status.statusMessage.value = msg
1956            raise samlRespError
1957
1958
1959        # Add credential into wallet
1960        #
1961        # Nb. if the certificates signature is invalid, it will be rejected
1962        log.debug("Adding credentials into wallet...")
1963        for assertion in response.assertions:
1964            self.addCredential(assertion)
1965
1966    def addCredential(self, 
1967                      credential, 
1968                      attributeAuthorityURI=None,
1969                      bUpdateCredentialRepository=False):
1970        """Add a new assertion to the list of assertion credentials held.
1971
1972        @type credential: SAML assertion
1973        @param credential: new assertion to be added
1974        @type attributeAuthorityURI: basestring
1975        @param attributeAuthorityURI: input the Attribute Authority URI from
1976        which credential was retrieved.  This is added to a dict to enable
1977        access to a given Attribute Certificate keyed by Attribute Authority
1978        URI. See the getCredential method.
1979        @type bUpdateCredentialRepository: bool
1980        @param bUpdateCredentialRepository: if set to True, and a repository
1981        exists it will be updated with the new credentials also
1982       
1983        @rtype: bool
1984        @return: True if certificate was added otherwise False.  - If an
1985        existing certificate from the same issuer has a later expiry it will
1986        take precedence and the new input certificate is ignored."""
1987       
1988        # Check input
1989        if not isinstance(credential, Assertion):
1990            raise CredentialWalletError("Input credential must be an "
1991                                        "%r type object" % Assertion)       
1992
1993        # Check to see if there is an existing Attribute Certificate held
1994        # that was issued by the same host.  If so, compare the expiry time.
1995        # The one with the latest expiry will be retained and the other
1996        # ingored
1997        bUpdateCred = True
1998        issuerName = attCert['issuerName']
1999       
2000        if issuerName in self.credentials:
2001            # There is an existing certificate held with the same issuing
2002            # host name as the new certificate
2003            attCertOld = self.credentials[issuerName]['attCert']
2004
2005            # Get expiry times in datetime format to allow comparison
2006            dtAttCertOldNotAfter = attCertOld.getValidityNotAfter(\
2007                                                            asDatetime=True)
2008            dtAttCertNotAfter = attCert.getValidityNotAfter(asDatetime=True)
2009
2010            # If the new certificate has an earlier expiry time then ignore it
2011            bUpdateCred = dtAttCertNotAfter > dtAttCertOldNotAfter
2012
2013               
2014        if bUpdateCred:
2015            # Update: Nb. -1 ID value flags item as new.  Items read in
2016            # from the CredentialRepository during creation of the wallet will
2017            # have +ve IDs previously allocated by the database
2018            self.credentials[issuerName] = {
2019                'id': -1, 
2020                'attCert': attCert,
2021                'issuerName': issuerName,
2022                'attributeAuthorityURI': attributeAuthorityURI
2023            }
2024
2025            if attributeAuthorityURI:
2026                self.credentialsKeyedByURI[attributeAuthorityURI] = \
2027                    self.credentials[issuerName]
2028           
2029            # Update the Credentials Repository - the permanent store of user
2030            # authorisation credentials.  This allows credentials for previous
2031            # sessions to be re-instated
2032            if bUpdateCredentialRepository:
2033                self.updateCredentialRepository()
2034
2035        # Flag to caller to indicate whether the input certificate was added
2036        # to the credentials or an exsiting certificate from the same issuer
2037        # took precedence
2038        return bUpdateCred
2039                       
2040    def audit(self):
2041        """Check the credentials held in the wallet removing any that have
2042        expired or are otherwise invalid."""
2043
2044        log.debug("CredentialWallet.audit ...")
2045       
2046        for key, val in self.credentials.items():
2047            if not self.isValidCredential(val['attCert']):
2048                del self.credentials[key]
2049
2050    def updateCredentialRepository(self, auditCred=True):
2051        msg = SamlCredentialWallet.CREDENTIAL_REPOSITORY_NOT_SUPPORTED_MSG
2052        log.warning(msg)
2053        warnings.warn(msg)
2054
2055    def isValidCredential(self, assertion):
2056        if utcNow < assertion.conditions.notBefore:
2057            msg = ('The current clock time [%s] is before the SAML Attribute '
2058                   'Response assertion conditions not before time [%s]' % 
2059                   (SAMLDateTime.toString(utcNow),
2060                    assertion.conditions.notBefore))
2061            log.warning(msg)
2062            return False
2063           
2064        if utcNow >= assertion.conditions.notOnOrAfter:
2065            msg = ('The current clock time [%s] is on or after the SAML '
2066                   'Attribute Response assertion conditions not on or after '
2067                   'time [%s]' % 
2068                   (SAMLDateTime.toString(utcNow),
2069                    assertion.conditions.notOnOrAfter))
2070            log.warning(msg)
2071            return False
2072           
2073        return True
2074       
2075       
2076class CredentialRepositoryError(_CredentialWalletException):   
2077    """Exception handling for NDG Credential Repository class."""
2078
2079
2080class CredentialRepository(object):
2081    """CredentialWallet's abstract interface class to a Credential Repository.
2082    The Credential Repository is abstract store of user currently valid user
2083    credentials.  It enables retrieval of attribute certificates from a user's
2084    previous session(s)"""
2085       
2086    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
2087        """Initialise Credential Repository abstract base class.  Derive from
2088        this class to define Credentail Repository interface Credential
2089        Wallet
2090
2091        If the connection string or properties file is set a connection
2092        will be made
2093
2094        @type dbPPhrase: string
2095        @param dbPPhrase: pass-phrase to database if applicable
2096       
2097        @type propFilePath: string
2098        @param propFilePath: file path to a properties file.  This could
2099        contain configuration parameters for the repository e.g.  database
2100        connection parameters
2101       
2102        @type **prop: dict
2103        @param **prop: any other keywords required
2104        """
2105        raise NotImplementedError(
2106            self.__init__.__doc__.replace('\n       ',''))
2107
2108
2109    def addUser(self, userId, dn=None):
2110        """A new user to Credentials Repository
2111       
2112        @type userId: string
2113        @param userId: userId for new user
2114        @type dn: string
2115        @param dn: users Distinguished Name (optional)"""
2116        raise NotImplementedError(
2117            self.addUser.__doc__.replace('\n       ',''))
2118
2119                           
2120    def auditCredentials(self, userId=None, **attCertValidKeys):
2121        """Check the attribute certificates held in the repository and delete
2122        any that have expired
2123
2124        @type userId: basestring/list or tuple
2125        @param userId: audit credentials for the input user ID or list of IDs
2126        @type attCertValidKeys: dict
2127        @param **attCertValidKeys: keywords which set how to check the
2128        Attribute Certificate e.g. check validity time, XML signature, version
2129         etc.  Default is check validity time only - See AttCert class"""
2130        raise NotImplementedError(
2131            self.auditCredentials.__doc__.replace('\n       ',''))
2132
2133
2134    def getCredentials(self, userId):
2135        """Get the list of credentials for a given users DN
2136       
2137        @type userId: string
2138        @param userId: users userId, name or X.509 cert. distinguished name
2139        @rtype: list
2140        @return: list of Attribute Certificates"""
2141        raise NotImplementedError(
2142            self.getCredentials.__doc__.replace('\n       ',''))
2143
2144       
2145    def addCredentials(self, userId, attCertList):
2146        """Add new attribute certificates for a user.  The user must have
2147        been previously registered in the repository
2148
2149        @type userId: string
2150        @param userId: users userId, name or X.509 cert. distinguished name
2151        @type attCertList: list
2152        @param attCertList: list of attribute certificates"""
2153        raise NotImplementedError(
2154            self.addCredentials.__doc__.replace('\n       ',''))
2155
2156
2157class NullCredentialRepository(CredentialRepository):
2158    """Implementation of Credential Repository interface with empty stubs. 
2159    Use this class in the case where no Credential Repository is required"""
2160   
2161    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
2162        pass
2163
2164    def addUser(self, userId):
2165        pass
2166                           
2167    def auditCredentials(self, **attCertValidKeys):
2168        pass
2169
2170    def getCredentials(self, userId):
2171        return []
2172       
2173    def addCredentials(self, userId, attCertList):
2174        pass
Note: See TracBrowser for help on using the repository browser.