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

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

Incomplete - task 2: XACML-Security Integration

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