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

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

Incomplete - task 2: XACML-Security Integration

  • simplified credential wallet - now a single class SAMLAssertionWallet for caching assertions containing authorisation decision statements and/or attribute statements
  • 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, assertions, verifyCredentials=True):
309        """Add a new assertion to the list of assertion credentials held.
310
311        @type assertions: iterable
312        @param assertions: 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 assertion in assertions:
320            if not isinstance(assertion, Assertion):
321                raise TypeError("Input credentials must be %r type; got %r" %
322                                (Assertion, assertion))
323               
324            elif verifyCredentials and not self.isValidCredential(assertion):
325                raise CredentialWalletError("Validity time error with "
326                                            "assertion %r" % assertion)
327       
328        # Any existing credentials are overwritten
329        self.__assertionsMap[key] = assertions
330
331    def retrieveCredentials(self, key):
332        """Retrieve credentials for the given key
333       
334        @param key: key index to credentials to retrieve
335        @type key: basestring
336        @rtype: iterable / None type if none found for key
337        @return: cached credentials indexed by input key
338        """
339        return self.__assertionsMap.get(key)
340                       
341    def audit(self):
342        """Check the credentials held in the wallet removing any that have
343        expired or are otherwise invalid."""
344
345        log.debug("SAMLAssertionWallet.audit ...")
346       
347        for k, v in self.__assertionsMap.items():
348            creds = [credential for credential in v
349                     if self.isValidCredential(credential)]
350            if len(creds) > 0:
351                self.__assertionsMap[k] = creds
352            else:
353                del self.__assertionsMap[k]
354
355    def isValidCredential(self, assertion):
356        """Validate SAML assertion time validity"""
357        utcNow = datetime.utcnow()
358        if utcNow < assertion.conditions.notBefore - self.clockSkewTolerance:
359            msg = ('The current clock time [%s] is before the SAML Attribute '
360                   'Response assertion conditions not before time [%s] ' 
361                   '(with clock skew tolerance = %s)' % 
362                   (SAMLDateTime.toString(utcNow),
363                    assertion.conditions.notBefore,
364                    self.clockSkewTolerance))
365            log.warning(msg)
366            return False
367           
368        if (utcNow >= 
369            assertion.conditions.notOnOrAfter + self.clockSkewTolerance):
370            msg = ('The current clock time [%s] is on or after the SAML '
371                   'Attribute Response assertion conditions not on or after '
372                   'time [%s] (with clock skew tolerance = %s)' % 
373                   (SAMLDateTime.toString(utcNow),
374                    assertion.conditions.notOnOrAfter,
375                    self.clockSkewTolerance))
376            log.warning(msg)
377            return False
378           
379        return True
380   
381    # Implement abstract method
382    updateCredentialRepository = lambda self: None
383   
384    def __getstate__(self):
385        '''Enable pickling for use with beaker.session'''
386        _dict = super(SAMLAssertionWallet, self).__getstate__()
387       
388        for attrName in SAMLAssertionWallet.__slots__:
389            # Ugly hack to allow for derived classes setting private member
390            # variables
391            if attrName.startswith('__'):
392                attrName = "_SAMLAssertionWallet" + attrName
393               
394            _dict[attrName] = getattr(self, attrName)
395           
396        return _dict
397       
398       
399class CredentialRepositoryError(_CredentialWalletException):   
400    """Exception handling for NDG Credential Repository class."""
401
402
403class CredentialRepository(object):
404    """CredentialWallet's abstract interface class to a Credential Repository.
405    The Credential Repository is abstract store of user currently valid user
406    credentials.  It enables retrieval of attribute certificates from a user's
407    previous session(s)"""
408    __metaclass__ = ABCMeta
409   
410    @abstractmethod
411    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
412        """Initialise Credential Repository abstract base class.  Derive from
413        this class to define Credentail Repository interface Credential
414        Wallet
415
416        If the connection string or properties file is set a connection
417        will be made
418
419        @type dbPPhrase: string
420        @param dbPPhrase: pass-phrase to database if applicable
421       
422        @type propFilePath: string
423        @param propFilePath: file path to a properties file.  This could
424        contain configuration parameters for the repository e.g.  database
425        connection parameters
426       
427        @type **prop: dict
428        @param **prop: any other keywords required
429        """
430        raise NotImplementedError(
431            self.__init__.__doc__.replace('\n       ',''))
432
433    @abstractmethod
434    def addUser(self, userId, dn=None):
435        """A new user to Credentials Repository
436       
437        @type userId: string
438        @param userId: userId for new user
439        @type dn: string
440        @param dn: users Distinguished Name (optional)"""
441        raise NotImplementedError(
442            self.addUser.__doc__.replace('\n       ',''))
443           
444    @abstractmethod
445    def auditCredentials(self, userId=None, **attCertValidKeys):
446        """Check the attribute certificates held in the repository and delete
447        any that have expired
448
449        @type userId: basestring/list or tuple
450        @param userId: audit credentials for the input user ID or list of IDs
451        @type attCertValidKeys: dict
452        @param **attCertValidKeys: keywords which set how to check the
453        Attribute Certificate e.g. check validity time, XML signature, version
454         etc.  Default is check validity time only - See AttCert class"""
455        raise NotImplementedError(
456            self.auditCredentials.__doc__.replace('\n       ',''))
457
458    @abstractmethod
459    def retrieveCredentials(self, userId):
460        """Get the list of credentials for a given users DN
461       
462        @type userId: string
463        @param userId: users userId, name or X.509 cert. distinguished name
464        @rtype: list
465        @return: list of credentials"""
466        raise NotImplementedError(
467            self.getCredentials.__doc__.replace('\n       ',''))
468
469    @abstractmethod       
470    def addCredentials(self, userId, credentialsList):
471        """Add credentials for a user.  The user must have
472        been previously registered in the repository
473
474        @type userId: string
475        @param userId: users userId, name or X.509 cert. distinguished name
476        @type credentialsList: list
477        @param credentialsList: list of credentials
478        """
479        raise NotImplementedError(
480            self.addCredentials.__doc__.replace('\n       ',''))
481
482
483class NullCredentialRepository(CredentialRepository):
484    """Implementation of Credential Repository interface with empty stubs. 
485    Use this class in the case where no Credential Repository is required"""
486   
487    def __init__(self, propFilePath=None, dbPPhrase=None, **prop):
488        """Null Credential Repository __init__ placeholder"""
489
490    def addUser(self, userId):
491        """Null Credential Repository addUser placeholder"""
492                           
493    def auditCredentials(self, **attCertValidKeys):
494        """Null Credential Repository addUser placeholder"""
495
496    def retrieveCredentials(self, userId):
497        """Null Credential Repository getCredentials placeholder"""
498        return []
499       
500    def addCredentials(self, userId, attCertList):
501        """Null Credential Repository addCredentials placeholder"""
Note: See TracBrowser for help on using the repository browser.