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

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