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

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

Incomplete - task 2: XACML-Security Integration

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