source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/CredWallet.py @ 3133

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

Major changes to enable trust based on multiple CAs and use of dynamically created user certs from MyProxy? SimpleCA - affects ...
python/ndg.security.server/ndg/security/server/AttAuthority/init.py,
python/ndg.security.test/ndg/security/test/AttAuthority/siteAAttAuthorityProperties.xml,
python/ndg.security.test/ndg/security/test/AttAuthority/siteBAttAuthorityProperties.xml,
python/ndg.security.server/ndg/security/server/conf/attAuthorityProperties.xml,
python/ndg.security.server/ndg/security/server/SessionMgr/init.py,
python/ndg.security.test/ndg/security/test/sessionMgrClient/sessionMgrProperties.xml,
python/ndg.security.server/ndg/security/server/conf/sessionMgrProperties.xml,
python/ndg.security.server/ndg/security/server/MyProxy.py,
python/ndg.security.test/ndg/security/test/sessionMgr/test.py,
python/ndg.security.common/ndg/security/common/CredWallet.py

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""NDG Credentials Wallet
2
3NERC Data Grid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "30/11/05"
7__copyright__ = "(C) 2007 STFC & NERC"
8__license__ = \
9"""This software may be distributed under the terms of the Q Public
10License, version 1.0 or later."""
11__contact__ = "P.J.Kershaw@rl.ac.uk"
12__revision__ = '$Id$'
13
14import logging
15log = logging.getLogger(__name__)
16
17# Temporary store of certificates for use with CredWallet getAttCert()
18import tempfile
19
20# Check Attribute Certificate validity times
21from datetime import datetime
22from datetime import timedelta
23
24
25# Access Attribute Authority's web service using ZSI - allow pass if not
26# loaded since it's possible to make AttAuthority instance locally without
27# using the WS
28aaImportError = True
29try:
30    # AttAuthority client package resides with CredWallet module in
31    # ndg.security.common
32    from ndg.security.common.AttAuthority import AttAuthorityClient, \
33        AttAuthorityClientError, AttributeRequestDenied, \
34        NoMatchingRoleInTrustedHosts
35    aaImportError = False
36   
37    # Reference 'X509PKIPathv1' BinarySecurityToken ValueType
38    from wsSecurity import SignatureHandler
39except ImportError:
40    log.warning('Loading CredWallet without SOAP interface imports')
41    pass
42
43# Likewise - may not want to use WS and use AttAuthority locally in which case
44# no need to import it
45try:
46    from ndg.security.server.AttAuthority import AttAuthority, \
47        AttAuthorityError, AttAuthorityAccessDenied
48    aaImportError = False
49except:
50    log.warning(\
51            'Loading CredWallet for SOAP interface to Attribute Authority')
52    pass
53
54if aaImportError:
55    raise ImportError, \
56        "Either AttAuthority or AttAuthorityClient classes must be " + \
57        "present to allow interoperation with Attribute Authorities"
58
59# Authentication X.509 Certificate
60from ndg.security.common.X509 import *
61from M2Crypto import X509, BIO, RSA
62
63# Authorisation - attribute certificate
64from ndg.security.common.AttCert import *
65
66
67class _CredWalletException(Exception):   
68    """Generic Exception class for CredWallet module.  Overrides Exception to
69    enable writing to the log"""
70    def __init__(self, msg):
71        log.error(msg)
72        Exception.__init__(self, msg)
73
74#_____________________________________________________________________________
75class CredWalletError(_CredWalletException):   
76    """Exception handling for NDG Credential Wallet class.  Overrides Exception to
77    enable writing to the log"""
78
79
80#_____________________________________________________________________________
81class CredWalletAttributeRequestDenied(CredWalletError):   
82    """Handling exception where CredWallet is denied authorisation by an
83    Attribute Authority.
84 
85    @type __extAttCertList: list
86    @ivar __extAttCertList: list of candidate Attribute Certificates that
87    could be used to try to get a mapped certificate from the target
88    Attribute Authority
89   
90    @type __trustedHostInfo: dict
91    @ivar __trustedHostInfo: dictionary indexed by host name giving
92    details of Attribute Authority URI and roles for trusted hosts"""
93   
94    def __init__(self, *args, **kw):
95        """Raise exception for attribute request denied with option to give
96        caller hint to certificates that could used to try to obtain a
97        mapped certificate
98       
99        @type extAttCertList: list
100        @param extAttCertList: list of candidate Attribute Certificates that
101        could be used to try to get a mapped certificate from the target
102        Attribute Authority
103       
104        @type trustedHostInfo: dict
105        @param trustedHostInfo: dictionary indexed by host name giving
106        details of Attribute Authority URI and roles for trusted hosts"""
107       
108        if 'trustedHostInfo' in kw:
109            self.__trustedHostInfo = kw['trustedHostInfo']
110            del kw['trustedHostInfo']
111        else:
112            self.__trustedHostInfo = {}
113           
114        if 'extAttCertList' in kw:
115            self.__extAttCertList = kw['extAttCertList']
116            del kw['extAttCertList']
117        else:   
118            self.__extAttCertList = []
119           
120        CredWalletError.__init__(self, *args, **kw)
121
122    def __getTrustedHostInfo(self):
123        """Get message text"""
124        return self.__msg
125
126    trustedHostInfo = property(fget=__getTrustedHostInfo, 
127                               doc="URI and roles details for trusted hosts")
128       
129    def __getExtAttCertList(self):
130        """Return list of candidate Attribute Certificates that could be used
131        to try to get a mapped certificate from the target Attribute Authority
132        """
133        return self.__extAttCertList
134
135    extAttCertList = property(fget=__getExtAttCertList,
136                              doc="list of candidate Attribute " + \
137                              "Certificates that could be used " + \
138                              "to try to get a mapped certificate " + \
139                              "from the target Attribute Authority")
140
141         
142#_____________________________________________________________________________
143class _MetaCredWallet(type):
144    """Enable CredWallet to have read only class variables e.g.
145   
146    print CredWallet.accessDenied
147   
148    ... is allowed but,
149   
150    CredWallet.accessDenied = None
151   
152    ... raises - AttributeError: can't set attribute"""
153   
154    def __getAccessDenied(cls):
155        '''accessDenied get method'''
156        return False
157   
158    accessDenied = property(fget=__getAccessDenied)
159   
160    def __getAccessGranted(cls):
161        '''accessGranted get method'''
162        return True
163   
164    accessGranted = property(fget=__getAccessGranted)
165
166#_____________________________________________________________________________       
167# CredWallet is a 'new-style' class inheriting from "object" and making use
168# of new Get/Set methods for hiding of attributes
169class CredWallet(object):
170    """Volatile store of user credentials associated with a user session
171   
172    @type __credRepos: ndg.security.common.CredRepos or derivative
173    @ivar __credRepos: reference to Credential Repository object.  An optional
174    non-volatile cache for storage of wallet info when
175
176    @type __mapFromTrustedHosts: bool
177    @ivar __mapFromTrustedHosts: if true, allow a mapped attribute certificate
178    to obtained in a getAttCert call.  Set false to prevent mappings.
179
180    @type __rtnExtAttCertList: bool
181    @ivar __rtnExtAttCertList: if true, return a list of external attribute
182    certificates from getAttCert call
183
184    @type __dn: ndg.security.common.X509.X500DN
185    @ivar __dn: distinguished name from user certificate
186
187    @type __credentials: dict       
188    @ivar __credentials: Credentials are stored as a dictionary one element per attribute
189    certicate held and indexed by certificate issuer name
190
191    @type __caCertFilePathList: basestring, list, tuple or None
192    @ivar __caCertFilePathList: file path(s) to CA certificates.  If None
193    then the input is quietly ignored.  See caCertFilePathList property
194
195    @type __userCert: ndg.security.common.X509.X509Cert
196    @ivar __userCert: X.509 user certificate instance
197
198    @type __issuingCert: ndg.security.common.X509.X509Cert
199    @ivar __issuingCert: X.509 user certificate instance
200 
201    @type __userPriKey: M2Crypto.RSA.RSA
202    @ivar __userPriKey: Private key used to sign outbound message
203    """
204
205    __metaclass__ = _MetaCredWallet
206   
207    def __init__(self,
208                 userCert,
209                 userPriKey,
210                 issuingCert=None,
211                 caCertFilePathList=None,
212                 aaURI=None,
213                 aaPropFilePath=None,
214                 credRepos=None,
215                 mapFromTrustedHosts=False,
216                 rtnExtAttCertList=True,
217                 attCertRefreshElapse=7200):
218        """Create store of user credentials for their current session
219
220        @type userCert: string / M2Crypto.X509.X509 /
221        ndg.security.common.X509.X509Cert
222        @param userCert: X.509 certificate for user
223       
224        @type userPriKey: string / M2Crypto.RSA.RSA
225        @param userPriKey: private key for user cert
226       
227        @type issuingCert: string / ndg.security.common.X509.X509Cert
228        @param issuingCert: X.509 cert for issuer of user cert
229       
230        @type aaURI: string
231        @param aaURI: URI of Attribute Authority to make requests to. 
232        Setting this ALSO creates an AttAuthorityClient instance
233        self.__aaClnt.  - See aaURI property for details.
234       
235        @type aaPropFilePath: string
236        @param aaPropFilePath: properties file path for an Attribute
237        Authority to make requests to.  Setting this ALSO creates an
238        AttAuthority instance self.__aa running locally.   - See aa property
239        for details.  aaURI takes precedence over this keyword i.e. if an
240        aaURI has been set, then calls are made to the AA web service at this
241        location rather to any self.__aa running locally.
242       
243        @type caCertFilePathList: string (for single file), list or tuple
244        @param caCertFilePathList: Certificate Authority's certificates - used
245        in validation of signed Attribute Certificates and WS-Security
246        signatures of incoming messages.  If not set here, it must
247        be input in call to getAttCert.
248               
249        @type credRepos: instance of CredRepos derived class
250        @param credRepos: Credential Repository instance.  If not set,
251        defaults to NullCredRepos type - see class below...
252       
253        @type mapFromTrustedHosts: bool
254        @param mapFromTrustedHosts sets behaviour for getAttCert().  If
255        set True and authorisation fails with the given Attribute Authority,
256        attempt to get authorisation using Attribute Certificates issued by
257        other trusted AAs.
258       
259        @type rtnExtAttCertList: bool
260        @param rtnExtAttCertList: behaviour for getAttCert().  If True, and
261        authorisation fails with the given Attribute Authority, return a list
262        of Attribute Certificates from other trusted AAs which could be used
263        to obtain a mapped Attribute Certificate on a subsequent authorisation
264        attempt
265       
266        @type attCertRefreshElapse: float / int
267        @param attCertRefreshElapse: used by getAttCert to determine
268        whether to replace an existing AC in the cache with a fresh one.  If
269        the existing one has less than attCertRefreshElapse time in seconds
270        left before expiry then replace it.
271        """
272
273        log.debug("Calling CredWallet.__init__ ...")
274       
275        self.attCertRefreshElapse = attCertRefreshElapse
276       
277        self.__setUserCert(userCert)
278        self.__setUserPriKey(userPriKey)
279        self.__setIssuingCert(issuingCert)
280       
281        self.__setAAuri(aaURI)
282        self.__setCAcertFilePathList(caCertFilePathList)
283               
284        self.__credRepos = credRepos or NullCredRepos()
285       
286        # Set behaviour for authorisation requests
287        self.__mapFromTrustedHosts = mapFromTrustedHosts
288        self.__rtnExtAttCertList = rtnExtAttCertList
289       
290       
291        # Get the distinguished name from the user certificate
292        self.__dn = self.__userCert.dn.serialise()
293       
294       
295        # Credentials are stored as a dictionary one element per attribute
296        # certicate held and indexed by certificate issuer name
297        self.__credentials = {}
298
299
300        # Make a connection to the Credentials Repository
301        if self.__credRepos:
302            log.info(\
303            'Checking CredentialRepository for credentials for user "%s"' % \
304                self.__dn)
305           
306            if not isinstance(self.__credRepos, CredRepos):
307                raise CredWalletError, \
308                    "Input Credentials Repository instance must be of a " + \
309                    "class derived from \"CredRepos\""
310   
311       
312            # Check for valid attribute certificates for the user
313            try:
314                self.__credRepos.auditCredentials(dn=self.__dn)
315                userCred = self.__credRepos.getCredentials(self.__dn)
316   
317            except Exception, e:
318                raise CredWalletError, \
319                "Error updating wallet with credentials from repository: " + \
320                    str(e)
321   
322   
323            # Update wallet with attribute certificates stored in the
324            # repository.  Store ID and certificate instantiated as an AttCert
325            # type
326            try:
327                for cred in userCred:
328                   
329                    attCert = AttCertParse(cred.attCert)
330                    issuerName = attCert['issuerName']
331                   
332                    self.__credentials[issuerName] = \
333                                             {'id':cred.id, 'attCert':attCert}   
334            except Exception, e:
335                try:
336                    raise CredWalletError, \
337                            "Error parsing Attribute Certificate ID '" + \
338                                    cred.id + "' retrieved from the " + \
339                                    "Credentials Repository: %s" % str(e)               
340                except:
341                    raise CredWalletError, "Error parsing Attribute " + \
342                                          "Certificate retrieved from " + \
343                                          "the Credentials Repository: %s:" \
344                                          % str(e)
345           
346           
347            # Filter out expired or otherwise invalid certificates
348            self.audit()
349
350       
351    #_________________________________________________________________________
352    def __setCert(self, cert):
353        """filter and convert input cert to signing verifying cert set
354        property methods.  For signingCert, set to None if it is not to be
355        included in the SOAP header.  For verifyingCert, set to None if this
356        cert can be expected to be retrieved from the SOAP header of the
357        message to be verified
358       
359        @type: ndg.security.common.X509.X509Cert / M2Crypto.X509.X509 /
360        string or None
361        @param cert: X.509 certificate. 
362       
363        @rtype ndg.security.common.X509.X509Cert
364        @return X.509 certificate object"""
365       
366        if cert is None or isinstance(cert, X509Cert):
367            # ndg.security.common.X509.X509Cert type / None
368            return cert
369           
370        elif isinstance(cert, X509.X509):
371            # M2Crypto.X509.X509 type
372            return X509Cert(m2CryptoX509=cert)
373           
374        elif isinstance(cert, basestring):
375            return X509CertParse(cert)
376       
377        else:
378            raise AttributeError, "X.509 Cert. must be type: " + \
379                "ndg.security.common.X509.X509Cert, M2Crypto.X509.X509 or " +\
380                "a base64 encoded string"
381
382
383    #_________________________________________________________________________
384    def __setUserCert(self, userCert):
385        "Set property method for X.509 user cert."
386        self.__userCert = self.__setCert(userCert)
387       
388
389    def __getUserCert(self):
390        """Get user cert X509Cert instance"""
391        return self.__userCert
392
393    userCert = property(fget=__getUserCert,
394                        fset=__setUserCert,
395                        doc="X.509 user certificate instance")
396
397
398    #_________________________________________________________________________
399    def __setIssuingCert(self, issuingCert):
400        "Set property method for X.509 user cert."
401        self.__issuingCert = self.__setCert(issuingCert)
402       
403
404    def __getIssuingCert(self):
405        """Get user cert X509Cert instance"""
406        return self.__issuingCert
407
408    issuingCert = property(fget=__getIssuingCert,
409                         fset=__setIssuingCert,
410                         doc="X.509 user certificate instance")
411     
412 
413    #_________________________________________________________________________
414    def __setUserPriKey(self, userPriKey):
415        """Set method for client private key
416       
417        Nb. if input is a string, userPriKeyPwd will need to be set if
418        the key is password protected.
419       
420        @type userPriKey: M2Crypto.RSA.RSA / string
421        @param userPriKey: private key used to sign message"""
422       
423        if isinstance(userPriKey, basestring):
424            self.__userPriKey = RSA.load_key_string(userPriKey,
425                                             callback=lambda *ar, **kw: None)
426        elif isinstance(userPriKey, RSA.RSA):
427            self.__userPriKey = userPriKey         
428        else:
429            raise AttributeError, "user private key must be a valid " + \
430                                  "M2Crypto.RSA.RSA type or a string"
431               
432    userPriKey = property(fset=__setUserPriKey,
433                          doc="Private key used to sign outbound message")
434
435   
436    def __getCredentials(self):
437        """Get Property method.  Credentials are read-only
438       
439        @rtype: dict
440        @return: cached ACs indesed by issuing organisation name"""
441        return self.__credentials
442
443    # Publish attribute
444    credentials = property(fget=__getCredentials,
445                           doc="List of Attribute Certificates")   
446
447
448    #_________________________________________________________________________
449    def __getCAcertFilePathList(self):
450        """Get CA cert or certs used to validate AC signatures and signatures
451        of peer SOAP messages.
452       
453        @rtype caCertFilePathList: basestring, list or tuple
454        @return caCertFilePathList: file path(s) to CA certificates."""
455        return self.__caCertFilePathList
456   
457    #_________________________________________________________________________
458    def __setCAcertFilePathList(self, caCertFilePathList):
459        """Set CA cert or certs to validate AC signatures, signatures
460        of Attribute Authority SOAP responses and SSL connections where
461        AA SOAP service is run over SSL.
462       
463        @type caCertFilePathList: basestring, list, tuple or None
464        @param caCertFilePathList: file path(s) to CA certificates.  If None
465        then the input is quietly ignored."""
466       
467        if isinstance(caCertFilePathList, basestring):
468           self.__caCertFilePathList = [caCertFilePathList]
469           
470        elif isinstance(caCertFilePathList, list):
471           self.__caCertFilePathList = caCertFilePathList
472           
473        elif isinstance(caCertFilePathList, tuple):
474           self.__caCertFilePathList = list(caCertFilePathList)
475
476        elif caCertFilePathList is not None:
477            raise CredWalletError, \
478                        "Input CA Certificate file path is not a valid string"     
479       
480    caCertFilePathList = property(fget=__getCAcertFilePathList,
481                                  fset=__setCAcertFilePathList,
482                                  doc="CA Certificates - used for " + \
483                                      "verification of AC and SOAP " + \
484                                      "message signatures and SSL " + \
485                                      "connections")
486
487
488    #_________________________________________________________________________
489    def __createAAClnt(self, aaURI):
490        """Set up a client to an Attribute Authority with the given URI
491       
492        @type aaURI: string
493        @param aaURI: Attribute Authority Web Service URI.
494
495        @rtype: ndg.security.common.AttAuthorityClient
496        @return: new Attribute Authority client instance"""
497
498        log.debug('CredWallet.__createAAClnt for service: "%s"' % aaURI)
499       
500        if self.__issuingCert is not None:
501            # Initialise WS-Security signature handling to pass
502            # BinarySecurityToken containing user cert and cert for user cert
503            # issuer
504            reqBinSecTokValType=SignatureHandler.binSecTokValType["X509PKIPathv1"]
505            certChain = (self.__issuingCert, self.__userCert)
506            signingCert = None
507        else:
508            # Pass user cert only - no need to pass a cert chain.  This type
509            # of token is more likely to be supported by the various
510            # WS-Security toolkits
511            reqBinSecTokValType=SignatureHandler.binSecTokValType["X509v3"]           
512            certChain = None
513            signingCert = self.__userCert
514       
515        aaClnt = AttAuthorityClient(uri=aaURI,
516                                reqBinSecTokValType=reqBinSecTokValType, 
517                                signingCertChain=certChain,
518                                signingCert=self.__userCert,
519                                signingPriKey=self.__userPriKey,
520                                caCertFilePathList=self.__caCertFilePathList,
521                                sslCACertFilePathList=caCertFilePathList)
522        return aaClnt
523
524
525    #_________________________________________________________________________
526    def __setAAuri(self, aaURI):
527        """Set property method for Attribute Authority Web Service URI to
528        connect to.  This method ALSO SETS UP THE CLIENT INTERFACE
529       
530        @type aaURI: string
531        @param aaURI: Attribute Authority Web Service URI.  Set to None to
532        initialise.  Set to a URI to instantiate a new AA client"""
533        if aaURI is None:
534            self.__aaClnt = None
535            return
536        else:
537            self.__aaClnt = self.__createAAClnt(aaURI)
538           
539    aaURI = property(fset=__setAAuri,
540             doc="AA URI - setting also sets up AttAuthorityClient instance!")
541
542
543    #_________________________________________________________________________
544    def __getAAclnt(self):
545        """Get property method for Attribute Authority Web Service client
546        instance.  Use aaURI propert to set up aaClnt
547       
548        @type aaClnt: AttAuthorityClient
549        @param aaClnt: Attribute Authority Web Service client instance"""
550        return self.__aaClnt
551           
552    aaClnt = property(fget=__getAAclnt, doc="AA web service client instance")
553
554
555    #_________________________________________________________________________
556    def __setAApropFilePath(self, aaPropFilePath):
557        """Set property method for the properties file of a local
558        Attribute Authority.  This method ALSO SETS UP THE LOCAL Attribute
559        Authority object to retrieve ACs from.  the property aaURI takes
560        precedence: if an aaURI is set then it assumed that an Attribute
561        Authority will be connected to via a web service call
562       
563        @type aaPropFilePath: string
564        @param aaPropFilePath: Attribute Authority properties file.  Setting
565        this instantiates a new AA locally"""
566        if aaPropFilePath is None:
567            self.__aa = None
568            return
569
570        # Make a new attribute authority instance
571        self.__aa = AttAuthority(propFilePath=aaPropFilePath)
572
573    aaPropFilePath = property(fset=__setAApropFilePath,
574    doc="AA properties file path - setting this also sets up an AA locally!")
575
576
577    #_________________________________________________________________________
578    def __getAA(self):
579        """Get property method for Attribute Authority Web Service client
580        instance.  Use aaURI propert to set up aaClnt
581       
582        @type aaClnt: AttAuthorityClient
583        @param aaClnt: Attribute Authority Web Service client instance"""
584        return self.__aaClnt
585           
586    aa = property(fget=__getAA, doc="Attribute Authority instance")
587
588
589    #_________________________________________________________________________
590    def isValid(self, **x509CertKeys):
591        """Check wallet's user cert.  If expired return False
592       
593        @type **x509CertKeys: dict
594        @param **x509CertKeys: keywords applying to
595        ndg.security.common.X509.X509Cert.isValidTime method"""
596        return self.__userCert.isValidTime(**x509CertKeys)
597
598   
599    #_________________________________________________________________________
600    def addCredential(self, attCert, bUpdateCredRepos=True):
601        """Add a new attribute certificate to the list of credentials held.
602
603        @type attCert:
604        @param attCert: new attribute Certificate to be added
605        @type bUpdateCredRepos: bool
606        @param bUpdateCredRepos: if set to True, and a repository exists it
607        will be updated with the new credentials also
608       
609        @rtype: bool
610        @return: True if certificate was added otherwise False.  - If an
611        existing certificate from the same issuer has a later expiry it will
612        take precence and the new input certificate is ignored."""
613
614        # Check input
615        if not isinstance(attCert, AttCert):
616            raise CredWalletError,\
617                "Attribute Certificate must be an AttCert type object"
618
619        # Check certificate validity
620        try:
621            attCert.isValid(raiseExcep=True)
622           
623        except AttCertError, e:
624            raise CredWalletError, "Adding Credential: %s" % e
625       
626
627        # Check to see if there is an existing Attribute Certificate held
628        # that was issued by the same host.  If so, compare the expiry time.
629        # The one with the latest expiry will be retained and the other
630        # ingored
631        bUpdateCred = True
632        issuerName = attCert['issuerName']
633       
634        if issuerName in self.__credentials:
635            # There is an existing certificate held with the same issuing
636            # host name as the new certificate
637            attCertOld = self.__credentials[issuerName]['attCert']
638
639            # Get expiry times in datetime format to allow comparison
640            dtAttCertOldNotAfter = attCertOld.getValidityNotAfter(\
641                                                            asDatetime=True)
642            dtAttCertNotAfter = attCert.getValidityNotAfter(asDatetime=True)
643
644            # If the new certificate has an earlier expiry time then ignore it
645            bUpdateCred = dtAttCertNotAfter > dtAttCertOldNotAfter
646
647               
648        if bUpdateCred:
649            # Update: Nb. -1 ID value flags item as new.  Items read in
650            # from the CredentialRepository during creation of the wallet will
651            # have +ve IDs previously allocated by the database
652            self.__credentials[issuerName] = {'id': -1, 'attCert': attCert}
653
654            # Update the Credentials Repository - the permanent store of user
655            # authorisation credentials.  This allows credentials for previous
656            # sessions to be re-instated
657            if self.__credRepos and bUpdateCredRepos:
658                self.updateCredRepos()
659
660        # Flag to caller to indicate whether the input certificate was added
661        # to the credentials or an exsiting certificate from the same issuer
662        # took precedence
663        return bUpdateCred
664           
665
666    #_________________________________________________________________________
667    def audit(self):
668        """Check the credentials held in the wallet removing any that have
669        expired or are otherwise invalid."""
670
671        log.debug("CredWallet.audit ...")
672       
673        # Nb. No signature check is carried out.  To do a check, access is
674        # needed to the cert of the CA that issued the Attribute Authority's
675        # cert
676        #
677        # P J Kershaw 12/09/05
678        for key, val in self.__credentials.items():
679            if not val['attCert'].isValid(chkSig=False):
680                del self.__credentials[key]
681
682
683    #_________________________________________________________________________           
684    def updateCredRepos(self, auditCred=True):
685        """Copy over non-persistent credentials held by wallet into the
686        perminent repository.
687       
688        @type auditCred: bool
689        @param auditCred: filter existing credentials in the repository
690        removing invalid ones"""
691
692        log.debug("CredWallet.updateCredRepos ...")
693       
694        if not self.__credRepos:
695            raise CredWalletError, \
696                  "No Credential Repository has been created for this wallet"
697                           
698        # Filter out invalid certs unless auditCred flag is explicitly set to
699        # false
700        if auditCred: self.audit()
701
702        # Update the database - only add new entries i.e. with an ID of -1
703        attCertList = [i['attCert'] for i in self.__credentials.values() \
704                        if i['id'] == -1]
705
706        self.__credRepos.addCredentials(self.__dn, attCertList)
707
708
709    #_________________________________________________________________________                   
710    def __getAttCert(self, aaClnt=None, extAttCert=None):       
711        """Wrapper to Attribute Authority attribute certificate request.  See
712        getAttCert for the classes' public interface.
713
714        To call the Attribute Authority as a Web Service, specify a URI
715        otherwise set the properties file path.
716       
717        If successful, a new attribute certificate is issued to the user
718        and added into the wallet
719
720        @type aaClnt: ndg.security.common.AttAuthorityClient
721        @param aaClnt: client object to Attribute Authority to make a request
722        to.  If omitted, it is set to self.__aaClnt.  This attribute may
723        itself be None.   In this case, a local AA client will be expected
724        set from a properties file.
725       
726        @type extAttCert: ndg.security.common.AttCert.AttCert
727        @param extAttCert: an existing Attribute Certificate which can
728        be used to making a mapping should the user not be registered with the
729        Attribute Authority"""
730     
731        log.debug("CredWallet.__getAttCert ...")
732       
733        if aaClnt is None:
734            aaClnt = self.__aaClnt
735           
736        if aaClnt is not None:
737            try:
738                attCert = aaClnt.getAttCert(userAttCert=extAttCert)
739                               
740                log.info(\
741             'Granted Attribute Certificate from issuer DN = "%s" at "%s"' % \
742             (attCert.issuerDN, aaClnt.uri))
743               
744            except AttributeRequestDenied, e:
745                raise CredWalletAttributeRequestDenied, str(e)
746                           
747        elif self.aaPropFilePath is not None:
748
749            # Call local based Attribute Authority with settings from the
750            # configuration file aaPropFilePath
751            try:
752                # Request a new attribute certificate from the Attribute
753                # Authority
754                attCert = self.__aa.getAttCert(userAttCert=extAttCert)
755               
756                log.info(\
757                     'Granted Attribute Certificate from issuer DN = "%s"' % \
758                     attCert.issuerDN)
759               
760            except AttAuthorityAccessDenied, e:
761                raise CredWalletAttributeRequestDenied, str(e)
762                       
763            except Exception, e:
764                raise CredWalletError,"Requesting attribute certificate: %s"%e
765
766        else:
767            raise CredWalletError, "Error requesting attribute: " + \
768                "certificate a URI or Attribute Authority configuration " + \
769                "file must be specified"
770       
771
772        # Update attribute Certificate instance with CA's certificate ready
773        # for signature check in addCredential()
774        if self.__caCertFilePathList is None:
775            raise CredWalletError, "No CA certificate has been set"
776       
777        attCert.certFilePathList = self.__caCertFilePathList
778
779       
780        # Add credential into wallet
781        #
782        # Nb. if the certificates signature is invalid, it will be rejected
783        self.addCredential(attCert)
784       
785        return attCert
786
787
788    #_________________________________________________________________________
789    def getAATrustedHostInfo(self, 
790                             userRole=None,
791                             aaPropFilePath=None,
792                             aaURI=None):
793        """Wrapper to Attribute Authority getTrustedHostInfo
794       
795        getAATrustedHostInfo([userRole=r, ][aaPropFilePath=f|aaURI=u])
796                   
797        @type userRole: string
798        @param userRole: get hosts which have a mapping to this role
799       
800        @type aaURI: string
801        @param aaURI: to call as a web service, specify the URI for the
802        Attribute Authority.
803       
804        @type aaPropFilePath: string
805        @param aaPropFilePath: Altenrative to aaURI - to run on the local
806        machine, specify the local Attribute Authority configuration file.
807        """
808       
809        log.debug(\
810        'CredWallet.getAATrustedHostInfo for role "%s" and service: "%s"' % \
811                   (userRole, aaURI or aaPropFilePath))
812        if aaURI:
813            self.__setAAuri(aaURI)
814        elif aaPropFilePath:
815            self.__setAAPropFilePath
816
817           
818        if self.__aaClnt is not None:
819            # Call Attribute Authority WS
820#            try:
821                return self.__aaClnt.getTrustedHostInfo(role=userRole)               
822#                           
823#            except Exception, e:
824#                raise CredWalletError, \
825#                            "Requesting trusted host information: %s" % str(e)               
826
827        elif self.__aa is not None:
828
829            # Call local based Attribute Authority with settings from the
830            # configuration file aaPropFilePath
831            try:
832                # Request a new attribute certificate from the Attribute
833                # Authority
834                return self.__aa.getTrustedHostInfo(role=userRole)
835               
836            except Exception, e:
837                raise CredWalletError, "Requesting trusted host info: %s" % e
838
839        else:
840            raise CredWalletError, "Error requesting trusted hosts info: " + \
841                                   "a URI or Attribute Authority " + \
842                                   "configuration file must be specified"
843
844
845    #_________________________________________________________________________
846    def getAttCert(self,
847                   reqRole=None,
848                   aaPropFilePath=None,
849                   aaURI=None,
850                   mapFromTrustedHosts=None,
851                   rtnExtAttCertList=None,
852                   extAttCertList=None,
853                   extTrustedHostList=None,
854                   refreshAttCert=False,
855                   attCertRefreshElapse=None):
856       
857        """For a given role, get an Attribute Certificate from an Attribute
858        Authority using a user's X.509 certificate.  If this fails try to make
859        a mapped Attribute Certificate by using a certificate from another
860        host which has a trust relationship to the Attribute Authority in
861        question.
862
863        getAttCert([reqRole=r, ][aaPropFilePath=f|aaURI=u,]
864                   [mapFromTrustedHosts=m, ]
865                   [rtnExtAttCertList=e, ][extAttCertList=el, ]
866                   [extTrustedHostList=et, ][refreshAttCert=ra])
867                 
868        The procedure is:
869
870        1) Try attribute request using user certificate
871        2) If the Attribute Authority (AA) doesn't recognise the certificate,
872        find out any other hosts which have a trust relationship to the AA.
873        3) Look for Attribute Certificates held in the wallet corresponding
874        to these hosts.
875        4) If no Attribute Certificates are available, call the relevant
876        hosts' AAs to get certificates
877        5) Finally, use these new certificates to try to obtain a mapped
878        certificate from the original AA
879        6) If this fails access is denied     
880                   
881        @type reqRole: string
882        @param reqRole: the required role to get access for
883       
884        @type aaURI: string
885        @param aaURI: to call as a web service, specify the URI for the
886        Attribute Authority.
887       
888        @type aaPropFilePath: string
889        @param aaPropFilePath: Altenrative to aaURI - to run on the local
890        machine, specify the local Attribute Authority configuration file.
891                               
892        @type mapFromTrustedHosts: bool / None     
893        @param mapFromTrustedHosts: if request fails via the user's cert
894        ID, then it is possible to get a mapped certificate by using
895        certificates from other AA's.  Set this flag to True, to allow this
896        second stage of generating a mapped certificate from the certificate
897        stored in the wallet credentials.
898
899        If set to False, it is possible to return the list of certificates
900        available for mapping and then choose which one or ones to use for
901        mapping by re-calling getAttCert with extAttCertList set to these
902        certificates.
903       
904        Defaults to None in which case self.__mapFromTrustedHosts is not
905        altered
906
907        The list is returned via CredWalletAttributeRequestDenied exception
908        If no value is set, the default value held in
909        self.__mapFromTrustedHosts is used
910
911        @type rtnExtAttCertList: bool / None
912        @param rtnExtAttCertList: If request fails, make a list of
913        candidate certificates from other Attribute Authorities which the user
914        could use to retry and get a mapped certificate.
915                               
916        If mapFromTrustedHosts is set True this flags value is overriden and
917        effectively set to True.
918
919        If no value is set, the default value held in self.__rtnExtAttCertList
920        is used.
921                               
922        The list is returned via a CredWalletAttributeRequestDenied exception
923        object.
924                               
925        @type extAttCertList: list
926        @param extAttCertList: Attribute Certificate or list of certificates
927        from other Attribute Authorities.  These can be used to get a mapped
928        certificate if access fails based on the user's certificate
929        credentials.  They are tried out in turn until access is granted so
930        the order of the list decides the order in which they will be tried
931
932        @type extTrustedHostList:
933        @param extTrustedHostList: same as extAttCertList keyword, but
934        instead of providing Attribute Certificates, give a list of Attribute
935        Authority hosts.  These will be matched up to Attribute Certificates
936        held in the wallet.  Matching certificates will then be used to try to
937        get a mapped Attribute Certificate.
938       
939        @type refreshAttCert: bool
940        @param refreshAttCert: if set to True, the attribute request
941        will go ahead even if the wallet already contains an Attribute
942        Certificate from the target Attribute Authority.  The existing AC in
943        the wallet will be replaced by the new one obtained from this call.
944                               
945        If set to False, this method will check to see if an AC issued by the
946        target AA already exists in the wallet.  If so, it will return this AC
947        to the caller without proceeding to make a call to the AA.
948       
949        @type attCertRefreshElapse: float / int
950        @param attCertRefreshElapse: determine whether to replace an
951        existing AC in the cache with a fresh one.  If the existing one has
952        less than attCertRefreshElapse time in seconds left before expiry then
953        replace it."""
954       
955        log.debug("CredWallet.getAttCert ...")
956       
957        if aaURI:
958            self.__setAAuri(aaURI)
959        elif aaPropFilePath:
960            self.__setAAPropFilePath
961           
962        if not refreshAttCert and self.__credentials:
963            # Refresh flag is not set so it's OK to check for any existing
964            # Attribute Certificate in the wallet whose issuerName match the
965            # target AA's name
966           
967            # Find out the site ID for the target AA by calling AA's host
968            # info WS method
969            log.debug("CredWallet.getAttCert - check AA site ID ...")
970           
971            try:
972                hostInfo = self.__aaClnt.getHostInfo()
973                aaName = hostInfo.keys()[0]
974            except Exception, e:
975                raise CredWalletError, "Getting host info: %s" % e
976           
977            # Look in the wallet for an AC with the same issuer name
978            if aaName in self.__credentials:
979                # Existing Attribute Certificate found in wallet - Check that
980                # it will be valid for at least the next 2 hours
981                if attCertRefreshElapse is not None:
982                    self.attCertRefreshElapse = attCertRefreshElapse
983                   
984                dtNow = datetime.utcnow() + \
985                        timedelta(seconds=self.attCertRefreshElapse)
986               
987                attCert = self.__credentials[aaName]['attCert']
988                if attCert.isValidTime(dtNow=dtNow):
989                    log.info("Retrieved an existing %s AC from the wallet" % \
990                             aaName)
991                    return attCert
992           
993           
994        # Check for settings from input, if not set use previous settings
995        # made
996        if mapFromTrustedHosts is not None:
997            self.__mapFromTrustedHosts = mapFromTrustedHosts
998
999        if rtnExtAttCertList is not None:
1000            self.__rtnExtAttCertList = rtnExtAttCertList
1001
1002
1003        # Check for list of external trusted hosts (other trusted NDG data
1004        # centres)
1005        if extTrustedHostList:
1006            log.info(\
1007        "Checking for ACs in wallet matching list of trusted hosts set: %s" % 
1008                 extTrustedHostList)
1009           
1010            if not self.__mapFromTrustedHosts:
1011                raise CredWalletError, "A list of trusted hosts has been " + \
1012                "input but mapping from trusted hosts is set to disallowed"
1013           
1014            if isinstance(extTrustedHostList, basestring):
1015                extTrustedHostList = [extTrustedHostList]
1016
1017            # Nb. Any extAttCertList is overriden by extTrustedHostList being
1018            # set
1019            extAttCertList = [self.__credentials[hostName]['attCert'] \
1020                              for hostName in extTrustedHostList \
1021                              if hostName in self.__credentials]
1022
1023        # Set an empty list to trigger an AttributeError by initialising it to
1024        # None
1025        if extAttCertList == []:
1026            extAttCertList = None
1027           
1028        # Repeat authorisation attempts until succeed or means are exhausted
1029        while True:
1030           
1031            # Check for candidate certificates for mapping
1032            try:
1033                # If list is set get the next cert
1034                extAttCert = extAttCertList.pop()
1035
1036            except AttributeError:
1037                log.debug(\
1038  "No external Attribute Certificates - trying request without mapping...")
1039                # No List set - attempt request without
1040                # using mapping from trusted hosts
1041                extAttCert = None
1042                           
1043            except IndexError:
1044               
1045                # List has been emptied without attribute request succeeding -
1046                # give up
1047                errMsg = "Attempting to obtained a mapped certificate: " + \
1048                    "no external attribute certificates are available"
1049                   
1050                # Add the exception form the last call to the Attribute
1051                # Authority if an error exists
1052                try:
1053                    errMsg += ": %s" % attributeRequestDenied
1054                except NameError:
1055                    pass
1056
1057                raise CredWalletAttributeRequestDenied, errMsg
1058                                                   
1059               
1060            # Request Attribute Certificate from Attribute Authority
1061            try:
1062                attCert = self.__getAttCert(extAttCert=extAttCert)               
1063                # Access granted
1064                return attCert
1065           
1066            except CredWalletAttributeRequestDenied, attributeRequestDenied:
1067                if not mapFromTrustedHosts and not rtnExtAttCertList:
1068                    # Creating a mapped certificate is not allowed - raise
1069                    # authorisation denied exception saved from earlier
1070                    raise attributeRequestDenied
1071
1072                if isinstance(extAttCertList, list):
1073                    # An list of attribute certificates from trusted hosts
1074                    # is present continue cycling through this until one of
1075                    # them is accepted and a mapped certificate can be derived
1076                    log.debug(\
1077"AC request denied - but external ACs available to try mapped AC request ...")
1078                    continue
1079                             
1080                #  Use the input required role and the AA's trusted host list
1081                # to identify attribute certificates from other hosts which
1082                # could be used to make a mapped certificate
1083                log.debug(\
1084                    "Getting a list of trusted hosts for mapped AC request ...")
1085                try:
1086                    trustedHostInfo = self.getAATrustedHostInfo(reqRole,
1087                                            aaPropFilePath=aaPropFilePath)
1088                except NoMatchingRoleInTrustedHosts, e:
1089                    raise CredWalletAttributeRequestDenied, \
1090                        'Can\'t get a mapped Attribute Certificate for ' + \
1091                        'the "%s" role' % reqRole
1092               
1093                except Exception, e:
1094                    raise CredWalletError, "Getting trusted hosts: %s" % e
1095
1096                if not trustedHostInfo:
1097                    raise CredWalletAttributeRequestDenied, \
1098                        "Attribute Authority has no trusted hosts with " + \
1099                        "which to make a mapping"
1100
1101               
1102                # Initialise external certificate list here - if none are
1103                # found IndexError will be raised on the next iteration and
1104                # an access denied error will be raised
1105                extAttCertList = []
1106
1107                # Look for Attribute Certificates with matching issuer host
1108                # names
1109                log.debug(\
1110            "Checking wallet for ACs issued by one of the trusted hosts...")
1111                for hostName in self.__credentials:
1112
1113                    # Nb. Candidate certificates for mappings must have
1114                    # original provenance and contain at least one of the
1115                    # required roles
1116                    attCert = self.__credentials[hostName]['attCert']
1117                   
1118                    if hostName in trustedHostInfo and attCert.isOriginal():                       
1119                        for role in attCert.roles:
1120                            if role in trustedHostInfo[hostName]['role']:                               
1121                                extAttCertList.append(attCert)
1122
1123
1124                if not extAttCertList:
1125                    log.debug("No wallet ACs matched any of the trusted " + \
1126                              "hosts.  - Try request for an AC from a " + \
1127                              "trusted host ...")
1128                   
1129                    # No certificates in the wallet matched the trusted host
1130                    # and required roles
1131                    #
1132                    # Try each host in turn in order to get a certificate with
1133                    # the required credentials in order to do a mapping
1134                    for host, info in trustedHostInfo.items():
1135                        try:
1136                            # Try request to trusted host
1137                            trustedAAClnt = self.__createAAClnt(info['aaURI'])
1138                            extAttCert=self.__getAttCert(aaClnt=trustedAAClnt)
1139
1140                            # Check the certificate contains at least one of
1141                            # the required roles
1142                            if [True for r in extAttCert.roles \
1143                                if r in info['role']]:
1144                               extAttCertList.append(extAttCert)
1145
1146                               # For efficiency, stop once obtained a valid
1147                               # cert - but may want complete list for user to
1148                               # choose from
1149                               #break
1150                               
1151                        except Exception, e:
1152                            # ignore any errors and continue
1153                            log.warning('AC request to trusted host "%s"' % \
1154                                        info['aaURI'] + ' resulted in: %s'%e)
1155                           
1156                   
1157                if not extAttCertList:                       
1158                    raise CredWalletAttributeRequestDenied, \
1159                        "No certificates are available with which to " + \
1160                        "make a mapping to the Attribute Authority"
1161
1162
1163                if not mapFromTrustedHosts:
1164                   
1165                    # Exit here returning the list of candidate certificates
1166                    # that could be used to make a mapped certificate
1167                    msg = "User is not registered with Attribute " + \
1168                          "Authority - retry using one of the returned " + \
1169                          "Attribute Certificates obtained from other " + \
1170                          "trusted hosts"
1171                         
1172                    raise CredWalletAttributeRequestDenied(msg,
1173                                            extAttCertList=extAttCertList,
1174                                            trustedHostInfo=trustedHostInfo)           
1175             
1176       
1177#_____________________________________________________________________________
1178class CredReposError(_CredWalletException):   
1179    """Exception handling for NDG Credential Repository class."""
1180
1181
1182#_____________________________________________________________________________
1183class CredRepos:
1184    """CredWallet's interface class to a Credential Repository"""
1185   
1186
1187    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
1188        """Initialise Credential Repository abstract base class derive from
1189        this class to define Credentail Repository interface Credential
1190        Wallet
1191
1192        If the connection string or properties file is set a connection
1193        will be made
1194
1195        @type dbPPhrase: string
1196        @param dbPPhrase: pass-phrase to database if applicable
1197       
1198        @type propFilePath: string
1199        @param propFilePath: file path to a properties file.  This could
1200        contain configuration parameters for the repository e.g.  database
1201        connection parameters
1202       
1203        @type **prop: dict
1204        @param **prop: any other keywords required
1205        """
1206        raise NotImplementedError, \
1207            self.__init__.__doc__.replace('\n       ','')
1208
1209
1210    def addUser(self, username, dn):
1211        """A new user to Credentials Repository
1212       
1213        @type username: string
1214        @param username: username for new user
1215        @type dn: string
1216        @param dn: users Distinguished Name"""
1217        raise NotImplementedError, \
1218            self.addUser.__doc__.replace('\n       ','')
1219
1220                           
1221    def auditCredentials(self, **attCertValidKeys):
1222        """Check the attribute certificates held in the repository and delete
1223        any that have expired
1224
1225        @type attCertValidKeys: dict
1226        @param **attCertValidKeys: keywords which set how to check the
1227        Attribute Certificate e.g. check validity time, XML signature, version
1228         etc.  Default is check validity time only - See AttCert class"""
1229        raise NotImplementedError, \
1230            self.auditCredentials.__doc__.replace('\n       ','')
1231
1232
1233    def getCredentials(self, dn):
1234        """Get the list of credentials for a given users DN
1235       
1236        @type dn: string
1237        @param dn: users distinguished name
1238        @rtype: list
1239        @return: list of Attribute Certificates"""
1240        raise NotImplementedError, \
1241            self.getCredentials.__doc__.replace('\n       ','')
1242
1243       
1244    def addCredentials(self, dn, attCertList):
1245        """Add new attribute certificates for a user.  The user must have
1246        been previously registered in the repository
1247
1248        @type dn: string
1249        @param dn: users Distinguished name
1250        @type attCertList: list
1251        @param attCertList: list of attribute certificates"""
1252        raise NotImplementedError, \
1253            self.addCredentials.__doc__.replace('\n       ','')
1254
1255
1256
1257#_____________________________________________________________________________
1258class NullCredRepos(CredRepos):
1259    """Implementation of Credential Repository interface with empty stubs. 
1260    This allows for where no Credential Repository is required"""
1261   
1262    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
1263        pass
1264
1265    def addUser(self, userName, dn):
1266        pass
1267                           
1268    def auditCredentials(self, **attCertValidKeys):
1269        pass
1270
1271    def getCredentials(self, dn):
1272        return []
1273       
1274    def addCredentials(self, dn, attCertList):
1275        pass
Note: See TracBrowser for help on using the repository browser.