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

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