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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/credentialwallet.py@6730
Revision 6730, 26.1 KB checked in by pjkersha, 11 years ago (diff)
  • Working Credential Wallet refactored for Python 2.6
  • Pruned out more old code: ZSI and Twisted SAOP/WSDL wrappers, Session Manager and WS-Security, Test 'Site B' Attribute Authority - for testing role mapping.
  • Started XACML package ElementTree based parser.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""Credential Wallet classes
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "30/11/05"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__license__ = "BSD - see LICENSE file in top-level directory"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = '$Id:credentialwallet.py 4378 2008-10-29 10:30:14Z pjkersha $'
11
12import logging
13log = logging.getLogger(__name__)
14logging.basicConfig(level=logging.DEBUG)
15
16import os
17import warnings
18
19# Check Attribute Certificate validity times
20from datetime import datetime, timedelta
21
22from ConfigParser import ConfigParser
23
24from ndg.saml.utils import SAMLDateTime
25from ndg.saml.saml2.core import Assertion
26
27from ndg.security.common.utils.configfileparsers import (     
28                                                    CaseSensitiveConfigParser,)
29
30
31class _CredentialWalletException(Exception):   
32    """Generic Exception class for CredentialWallet module.  Overrides
33    Exception to enable writing to the log"""
34    def __init__(self, *arg, **kw):
35        if len(arg) > 0:
36            log.error(arg[0])
37           
38        Exception.__init__(self, *arg, **kw)
39
40
41class CredentialWalletError(_CredentialWalletException):   
42    """Exception handling for NDG Credential Wallet class.  Overrides Exception
43    to enable writing to the log"""
44
45
46class CredentialWalletAttributeRequestDenied(CredentialWalletError):   
47    """Handling exception where CredentialWallet is denied authorisation by an
48    Attribute Authority.
49 
50    @type __extAttCertList: list
51    @ivar __extAttCertList: list of candidate Attribute Certificates that
52    could be used to try to get a mapped certificate from the target
53    Attribute Authority
54   
55    @type __trustedHostInfo: dict
56    @ivar __trustedHostInfo: dictionary indexed by host name giving
57    details of Attribute Authority URI and roles for trusted hosts"""
58   
59    def __init__(self, *args, **kw):
60        """Raise exception for attribute request denied with option to give
61        caller hint to certificates that could used to try to obtain a
62        mapped certificate
63       
64        @type extAttCertList: list
65        @param extAttCertList: list of candidate Attribute Certificates that
66        could be used to try to get a mapped certificate from the target
67        Attribute Authority
68       
69        @type trustedHostInfo: dict
70        @param trustedHostInfo: dictionary indexed by host name giving
71        details of Attribute Authority URI and roles for trusted hosts"""
72       
73        self.__trustedHostInfo = kw.pop('trustedHostInfo', {})
74        self.__extAttCertList = kw.pop('extAttCertList', [])
75           
76        CredentialWalletError.__init__(self, *args, **kw)
77
78    def _getTrustedHostInfo(self):
79        """Get message text"""
80        return self.__trustedHostInfo
81
82    trustedHostInfo = property(fget=_getTrustedHostInfo, 
83                               doc="URI and roles details for trusted hosts")
84       
85    def _getExtAttCertList(self):
86        """Return list of candidate Attribute Certificates that could be used
87        to try to get a mapped certificate from the target Attribute Authority
88        """
89        return self.__extAttCertList
90
91    extAttCertList = property(fget=_getExtAttCertList,
92                              doc="list of candidate Attribute Certificates "
93                              "that could be used to try to get a mapped "
94                              "certificate from the target Attribute "
95                              "Authority")
96
97         
98class _MetaCredentialWallet(type):
99    """Enable CredentialWallet to have read only class variables e.g.
100   
101    print CredentialWallet.accessDenied
102   
103    ... is allowed but,
104   
105    CredentialWallet.accessDenied = None
106   
107    ... raises - AttributeError: can't set attribute"""
108   
109    def _getAccessDenied(cls):
110        '''accessDenied get method'''
111        return False
112   
113    accessDenied = property(fget=_getAccessDenied)
114   
115    def _getAccessGranted(cls):
116        '''accessGranted get method'''
117        return True
118   
119    accessGranted = property(fget=_getAccessGranted)
120
121class CredentialContainer(object):
122    """Container for cached credentials"""
123    ID_ATTRNAME = 'id'
124    ITEM_ATTRNAME = 'credential'
125    ISSUERNAME_ATTRNAME = 'issuerName'
126    ATTRIBUTE_AUTHORITY_URI_ATTRNAME = 'attributeAuthorityURI'
127    CREDENTIAL_TYPE_ATTRNAME = 'type'
128   
129    __ATTRIBUTE_NAMES = (
130        ID_ATTRNAME,
131        ITEM_ATTRNAME,
132        ISSUERNAME_ATTRNAME,
133        ATTRIBUTE_AUTHORITY_URI_ATTRNAME,
134        CREDENTIAL_TYPE_ATTRNAME
135    )
136    __slots__ = tuple(["__%s" % n for n in __ATTRIBUTE_NAMES])
137   
138    def __init__(self, _type=None):
139        self.__type = None
140        self.type = _type
141       
142        self.__id = -1
143        self.__credential = None
144        self.__issuerName = None
145        self.__attributeAuthorityURI = None
146
147    def _getType(self):
148        return self.__type
149
150    def _setType(self, value):
151        if not isinstance(value, type):
152            raise TypeError('Expecting %r for "type" attribute; got %r' %
153                            (type, type(value)))       
154        self.__type = value
155
156    type = property(_getType, _setType, 
157                    doc="Type for credential - set to None for any type")
158
159    def _getId(self):
160        return self.__id
161
162    def _setId(self, value):
163        if not isinstance(value, int):
164            raise TypeError('Expecting int type for "id" attribute; got %r' %
165                            type(value))
166        self.__id = value
167
168    id = property(_getId, 
169                  _setId, 
170                  doc="Numbered identifier for credential - "
171                      "set to -1 for new credentials")
172
173    def _getCredential(self):
174        return self.__credential
175
176    def _setCredential(self, value):
177        # Safeguard type attribute referencing for unpickling process - this
178        # method may be called before type attribute has been set
179        _type = getattr(self, 
180                        CredentialContainer.CREDENTIAL_TYPE_ATTRNAME, 
181                        None)
182       
183        if _type is not None and not isinstance(value, _type):
184            raise TypeError('Expecting %r type for "credential" attribute; '
185                            'got %r' % type(value))
186        self.__credential = value
187
188    credential = property(_getCredential, 
189                          _setCredential, 
190                          doc="Credential object")
191
192    def _getIssuerName(self):
193        return self.__issuerName
194
195    def _setIssuerName(self, value):
196        self.__issuerName = value
197
198    issuerName = property(_getIssuerName, 
199                          _setIssuerName, 
200                          doc="Name of issuer of the credential")
201
202    def _getAttributeAuthorityURI(self):
203        return self.__attributeAuthorityURI
204
205    def _setAttributeAuthorityURI(self, value):
206        """String or None type are allowed - The URI may be set to None if
207        a local Attribute Authority instance is being invoked rather
208        one hosted via a remote URI
209        """
210        if not isinstance(value, (basestring, type(None))):
211            raise TypeError('Expecting string or None type for '
212                            '"attributeAuthorityURI"; got %r instead' % 
213                            type(value))
214        self.__attributeAuthorityURI = value
215
216    attributeAuthorityURI = property(_getAttributeAuthorityURI,
217                                     _setAttributeAuthorityURI, 
218                                     doc="Attribute Authority Service URI")
219
220    def __getstate__(self):
221        '''Enable pickling'''
222        thisDict = dict([(attrName, getattr(self, attrName))
223                         for attrName in CredentialContainer.__ATTRIBUTE_NAMES])
224       
225        return thisDict
226       
227    def __setstate__(self, attrDict):
228        '''Enable pickling for use with beaker.session'''
229        try:
230            for attr, val in attrDict.items():
231                setattr(self, attr, val)
232        except Exception, e:
233            pass
234       
235
236class CredentialWalletBase(object):
237    """Abstract base class for NDG and SAML Credential Wallet implementations
238    """ 
239    CONFIG_FILE_OPTNAMES = ("userId", )
240    __slots__ = ("__credentials", "__credentialsKeyedByURI", "__userId")
241   
242    def __init__(self):
243        self.__userId = None
244        self.__credentials = {}
245        self.__credentialsKeyedByURI = {}
246
247    @classmethod
248    def fromConfig(cls, cfg, **kw):
249        '''Alternative constructor makes object from config file settings
250        @type cfg: basestring /ConfigParser derived type
251        @param cfg: configuration file path or ConfigParser type object
252        @rtype: ndg.security.common.credentialWallet.SAMLCredentialWallet
253        @return: new instance of this class
254        '''
255        credentialWallet = cls()
256        credentialWallet.parseConfig(cfg, **kw)
257       
258        return credentialWallet
259
260    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
261        '''Virtual method defines interface to read config file settings
262        @type cfg: basestring /ConfigParser derived type
263        @param cfg: configuration file path or ConfigParser type object
264        @type prefix: basestring
265        @param prefix: prefix for option names e.g. "certExtApp."
266        @type section: baestring
267        @param section: configuration file section from which to extract
268        parameters.
269        '''
270        raise NotImplementedError(CredentialWalletBase.parseConfig.__doc__)
271
272    def addCredential(self, 
273                      credential, 
274                      attributeAuthorityURI=None,
275                      bUpdateCredentialRepository=True):
276        """Add a new attribute certificate to the list of credentials held.
277
278        @type credential: determined by derived class implementation e.g.
279        SAML assertion
280        @param credential: new attribute Certificate to be added
281        @type attributeAuthorityURI: basestring
282        @param attributeAuthorityURI: input the Attribute Authority URI from
283        which credential was retrieved.  This is added to a dict to enable
284        access to a given Attribute Certificate keyed by Attribute Authority
285        URI. See the getCredential method.
286        @type bUpdateCredentialRepository: bool
287        @param bUpdateCredentialRepository: if set to True, and a repository
288        exists it will be updated with the new credentials also
289       
290        @rtype: bool
291        @return: True if certificate was added otherwise False.  - If an
292        existing certificate from the same issuer has a later expiry it will
293        take precedence and the new input certificate is ignored."""
294        raise NotImplementedError(CredentialWalletBase.addCredential.__doc__)
295           
296    def audit(self):
297        """Check the credentials held in the wallet removing any that have
298        expired or are otherwise invalid."""
299        raise NotImplementedError(CredentialWalletBase.audit.__doc__)
300
301    def updateCredentialRepository(self, auditCred=True):
302        """Copy over non-persistent credentials held by wallet into the
303        perminent repository.
304       
305        @type auditCred: bool
306        @param auditCred: filter existing credentials in the repository
307        removing invalid ones"""
308        raise NotImplementedError(
309                    CredentialWalletBase.updateCredentialRepository.__doc__)
310       
311    def _getCredentials(self):
312        """Get Property method.  Credentials dict is read-only but also see
313        addCredential method
314       
315        @rtype: dict
316        @return: cached ACs indesed by issuing organisation name"""
317        return self.__credentials
318
319    # Publish attribute
320    credentials = property(fget=_getCredentials,
321                           doc="List of credentials linked to issuing "
322                               "authorities")
323
324    def _getCredentialsKeyedByURI(self):
325        """Get Property method for credentials keyed by Attribute Authority URI
326        Credentials dict is read-only but also see addCredential method
327       
328        @rtype: dict
329        @return: cached ACs indexed by issuing Attribute Authority"""
330        return self.__credentialsKeyedByURI
331   
332    # Publish attribute
333    credentialsKeyedByURI = property(fget=_getCredentialsKeyedByURI,
334                                     doc="List of Attribute Certificates "
335                                         "linked to attribute authority URI")
336       
337    def _getUserId(self):
338        return self.__userId
339
340    def _setUserId(self, value):
341        if not isinstance(value, basestring):
342            raise TypeError('Expecting string type for "userId"; got %r '
343                            'instead' % type(value))
344        self.__userId = value
345
346    userId = property(_getUserId, _setUserId, 
347                      doc="User Identity for this wallet")
348
349    def __getstate__(self):
350        '''Enable pickling for use with beaker.session'''
351        _dict = {}
352        for attrName in CredentialWalletBase.__slots__:
353            # Ugly hack to allow for derived classes setting private member
354            # variables
355            if attrName.startswith('__'):
356                attrName = "_CredentialWalletBase" + attrName
357               
358            _dict[attrName] = getattr(self, attrName)
359           
360        return _dict
361 
362    def __setstate__(self, attrDict):
363        '''Enable pickling for use with beaker.session'''
364        for attrName, val in attrDict.items():
365            setattr(self, attrName, val)
366
367
368class SAMLCredentialWallet(CredentialWalletBase):
369    """CredentialWallet for Earth System Grid supporting caching of SAML
370    Attribute Assertions
371    """
372    CONFIG_FILE_OPTNAMES = CredentialWalletBase.CONFIG_FILE_OPTNAMES + (
373                           "clockSkewTolerance", )
374    __slots__ = ("__clockSkewTolerance",)
375   
376    CREDENTIAL_REPOSITORY_NOT_SUPPORTED_MSG = ("SAMLCredentialWallet doesn't "
377                                               "support the "
378                                               "CredentialRepository "
379                                               "interface")
380
381    def __init__(self):
382        super(SAMLCredentialWallet, self).__init__()
383        self.__clockSkewTolerance = timedelta(seconds=0.)
384
385    def _getClockSkewTolerance(self):
386        return self.__clockSkewTolerance
387
388    def _setClockSkewTolerance(self, value):
389        if isinstance(value, (float, int, long)):
390            self.__clockSkewTolerance = timedelta(seconds=value)
391           
392        elif isinstance(value, basestring):
393            self.__clockSkewTolerance = timedelta(seconds=float(value))
394        else:
395            raise TypeError('Expecting float, int, long or string type for '
396                            '"clockSkewTolerance"; got %r' % type(value))
397
398    clockSkewTolerance = property(_getClockSkewTolerance, 
399                                  _setClockSkewTolerance, 
400                                  doc="Allow a tolerance (seconds) for "
401                                      "checking timestamps of the form: "
402                                      "notBeforeTime - tolerance < now < "
403                                      "notAfterTime + tolerance")
404
405    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
406        '''Read config file settings
407        @type cfg: basestring /ConfigParser derived type
408        @param cfg: configuration file path or ConfigParser type object
409        @type prefix: basestring
410        @param prefix: prefix for option names e.g. "certExtApp."
411        @type section: baestring
412        @param section: configuration file section from which to extract
413        parameters.
414        ''' 
415        if isinstance(cfg, basestring):
416            cfgFilePath = os.path.expandvars(cfg)
417            _cfg = CaseSensitiveConfigParser()
418            _cfg.read(cfgFilePath)
419           
420        elif isinstance(cfg, ConfigParser):
421            _cfg = cfg   
422        else:
423            raise AttributeError('Expecting basestring or ConfigParser type '
424                                 'for "cfg" attribute; got %r type' % type(cfg))
425       
426        prefixLen = len(prefix)
427        for optName, val in _cfg.items(section):
428            if prefix and optName.startswith(prefix):
429                optName = optName[prefixLen:]
430               
431            setattr(self, optName, val)
432
433    def addCredential(self, 
434                      credential, 
435                      attributeAuthorityURI=None,
436                      bUpdateCredentialRepository=False,
437                      verifyCredential=True):
438        """Add a new assertion to the list of assertion credentials held.
439
440        @type credential: SAML assertion
441        @param credential: new assertion to be added
442        @type attributeAuthorityURI: basestring
443        @param attributeAuthorityURI: input the Attribute Authority URI from
444        which credential was retrieved.  This is added to a dict to enable
445        access to a given Attribute Certificate keyed by Attribute Authority
446        URI. See the getCredential method.
447        @type bUpdateCredentialRepository: bool
448        @param bUpdateCredentialRepository: if set to True, and a repository
449        exists it will be updated with the new credentials also. Nb. a derived
450        class will need to be implemented to enable this capability - see
451        the updateCredentialRepository method.
452        @type verifyCredential: bool
453        @param verifyCredential: if set to True, test validity of credential
454        by calling isValidCredential method.
455       
456        @rtype: bool
457        @return: True if credential was added otherwise False.  - If an
458        existing certificate from the same issuer has a later expiry it will
459        take precedence and the new input certificate is ignored."""
460       
461        # Check input
462        if not isinstance(credential, Assertion):
463            raise CredentialWalletError("Input credential must be an "
464                                        "%r type object" % Assertion)       
465
466        if verifyCredential and not self.isValidCredential(credential):
467            raise CredentialWalletError("Validity time error with assertion %r"
468                                        % credential)
469       
470        # Check to see if there is an existing Attribute Certificate held
471        # that was issued by the same host.  If so, compare the expiry time.
472        # The one with the latest expiry will be retained and the other
473        # ignored
474        bUpdateCred = True
475        if credential.issuer is None:
476            raise AttributeError("Adding SAML assertion to wallet: no issuer "
477                                 "set")
478           
479        issuerName = credential.issuer.value
480       
481        if issuerName in self.credentials:
482            # There is an existing certificate held with the same issuing
483            # host name as the new certificate
484            credentialOld = self.credentials[issuerName].credential
485
486            # If the new certificate has an earlier expiry time then ignore it
487            bUpdateCred = (credential.conditions.notOnOrAfter > 
488                           credentialOld.conditions.notOnOrAfter)
489
490        if bUpdateCred:
491            thisCredential = CredentialContainer(Assertion)
492            thisCredential.credential = credential
493            thisCredential.issuerName = issuerName
494            thisCredential.attributeAuthorityURI = attributeAuthorityURI
495           
496            self.credentials[issuerName] = thisCredential
497           
498            if attributeAuthorityURI:
499                self.credentialsKeyedByURI[
500                    attributeAuthorityURI] = thisCredential
501
502            # Update the Credentials Repository - the permanent store of user
503            # authorisation credentials.  This allows credentials for previous
504            # sessions to be re-instated
505            if bUpdateCredentialRepository:
506                self.updateCredentialRepository()
507
508        # Flag to caller to indicate whether the input certificate was added
509        # to the credentials or an exsiting certificate from the same issuer
510        # took precedence
511        return bUpdateCred
512                       
513    def audit(self):
514        """Check the credentials held in the wallet removing any that have
515        expired or are otherwise invalid."""
516
517        log.debug("SAMLCredentialWallet.audit ...")
518       
519        for issuerName, issuerEntry in self.credentials.items():
520            if not self.isValidCredential(issuerEntry.credential):
521                self.credentialsKeyedByURI.pop(
522                    issuerEntry.attributeAuthorityURI,
523                    None)
524                   
525                del self.credentials[issuerName]
526
527    def updateCredentialRepository(self, auditCred=True):
528        """No Credential Repository support is required"""
529        msg = SAMLCredentialWallet.CREDENTIAL_REPOSITORY_NOT_SUPPORTED_MSG
530        log.warning(msg)
531        warnings.warn(msg)
532
533    def isValidCredential(self, assertion):
534        """Validate SAML assertion time validity"""
535        utcNow = datetime.utcnow()
536        if utcNow < assertion.conditions.notBefore - self.clockSkewTolerance:
537            msg = ('The current clock time [%s] is before the SAML Attribute '
538                   'Response assertion conditions not before time [%s] ' 
539                   '(with clock skew tolerance = %s)' % 
540                   (SAMLDateTime.toString(utcNow),
541                    assertion.conditions.notBefore,
542                    self.clockSkewTolerance))
543            log.warning(msg)
544            return False
545           
546        if (utcNow >= 
547            assertion.conditions.notOnOrAfter + self.clockSkewTolerance):
548            msg = ('The current clock time [%s] is on or after the SAML '
549                   'Attribute Response assertion conditions not on or after '
550                   'time [%s] (with clock skew tolerance = %s)' % 
551                   (SAMLDateTime.toString(utcNow),
552                    assertion.conditions.notOnOrAfter,
553                    self.clockSkewTolerance))
554            log.warning(msg)
555            return False
556           
557        return True
558   
559    def __getstate__(self):
560        '''Enable pickling for use with beaker.session'''
561        _dict = super(SAMLCredentialWallet, self).__getstate__()
562       
563        for attrName in SAMLCredentialWallet.__slots__:
564            # Ugly hack to allow for derived classes setting private member
565            # variables
566            if attrName.startswith('__'):
567                attrName = "_SAMLCredentialWallet" + attrName
568               
569            _dict[attrName] = getattr(self, attrName)
570           
571        return _dict
572       
573       
574class CredentialRepositoryError(_CredentialWalletException):   
575    """Exception handling for NDG Credential Repository class."""
576
577
578class CredentialRepository(object):
579    """CredentialWallet's abstract interface class to a Credential Repository.
580    The Credential Repository is abstract store of user currently valid user
581    credentials.  It enables retrieval of attribute certificates from a user's
582    previous session(s)"""
583       
584    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
585        """Initialise Credential Repository abstract base class.  Derive from
586        this class to define Credentail Repository interface Credential
587        Wallet
588
589        If the connection string or properties file is set a connection
590        will be made
591
592        @type dbPPhrase: string
593        @param dbPPhrase: pass-phrase to database if applicable
594       
595        @type propFilePath: string
596        @param propFilePath: file path to a properties file.  This could
597        contain configuration parameters for the repository e.g.  database
598        connection parameters
599       
600        @type **prop: dict
601        @param **prop: any other keywords required
602        """
603        raise NotImplementedError(
604            self.__init__.__doc__.replace('\n       ',''))
605
606
607    def addUser(self, userId, dn=None):
608        """A new user to Credentials Repository
609       
610        @type userId: string
611        @param userId: userId for new user
612        @type dn: string
613        @param dn: users Distinguished Name (optional)"""
614        raise NotImplementedError(
615            self.addUser.__doc__.replace('\n       ',''))
616
617                           
618    def auditCredentials(self, userId=None, **attCertValidKeys):
619        """Check the attribute certificates held in the repository and delete
620        any that have expired
621
622        @type userId: basestring/list or tuple
623        @param userId: audit credentials for the input user ID or list of IDs
624        @type attCertValidKeys: dict
625        @param **attCertValidKeys: keywords which set how to check the
626        Attribute Certificate e.g. check validity time, XML signature, version
627         etc.  Default is check validity time only - See AttCert class"""
628        raise NotImplementedError(
629            self.auditCredentials.__doc__.replace('\n       ',''))
630
631
632    def getCredentials(self, userId):
633        """Get the list of credentials for a given users DN
634       
635        @type userId: string
636        @param userId: users userId, name or X.509 cert. distinguished name
637        @rtype: list
638        @return: list of Attribute Certificates"""
639        raise NotImplementedError(
640            self.getCredentials.__doc__.replace('\n       ',''))
641
642       
643    def addCredentials(self, userId, attCertList):
644        """Add new attribute certificates for a user.  The user must have
645        been previously registered in the repository
646
647        @type userId: string
648        @param userId: users userId, name or X.509 cert. distinguished name
649        @type attCertList: list
650        @param attCertList: list of attribute certificates"""
651        raise NotImplementedError(
652            self.addCredentials.__doc__.replace('\n       ',''))
653
654
655class NullCredentialRepository(CredentialRepository):
656    """Implementation of Credential Repository interface with empty stubs. 
657    Use this class in the case where no Credential Repository is required"""
658   
659    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
660        """Null Credential Repository __init__ placeholder"""
661
662    def addUser(self, userId):
663        """Null Credential Repository addUser placeholder"""
664                           
665    def auditCredentials(self, **attCertValidKeys):
666        """Null Credential Repository addUser placeholder"""
667
668    def getCredentials(self, userId):
669        """Null Credential Repository getCredentials placeholder"""
670        return []
671       
672    def addCredentials(self, userId, attCertList):
673        """Null Credential Repository addCredentials placeholder"""
Note: See TracBrowser for help on using the repository browser.