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

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

Unit tested SamlCredentialWallet?.

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