source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/xacml/pip/saml_pip.py @ 7698

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg-security/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/xacml/pip/saml_pip.py@7698
Revision 7698, 23.3 KB checked in by pjkersha, 10 years ago (diff)

Integrated SAML ESGF Group/Role? attribute value type into SAML Attribute Authority client unit tests.

Line 
1"""Module for XACML Policy Information Point with SAML interface to
2Attribute Authority
3
4"""
5__author__ = "P J Kershaw"
6__date__ = "06/08/10"
7__copyright__ = "(C) 2010 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:$'
11import logging
12log = logging.getLogger(__name__)
13
14from os import path
15from ConfigParser import SafeConfigParser, ConfigParser
16import base64
17
18import beaker.session
19
20from ndg.xacml.core.attributedesignator import SubjectAttributeDesignator
21from ndg.xacml.core.attribute import Attribute as XacmlAttribute
22from ndg.xacml.core.attributevalue import AttributeValueClassFactory as \
23    XacmlAttributeValueClassFactory
24from ndg.xacml.core.context.pipinterface import PIPInterface
25from ndg.xacml.core.context.request import Request as XacmlRequestCtx
26
27from ndg.saml.saml2.core import (AttributeQuery as SamlAttributeQuery, 
28                                 Attribute as SamlAttribute)
29from ndg.saml.utils import TypedList as SamlTypedList
30from ndg.saml.saml2.binding.soap.client.attributequery import \
31                                            AttributeQuerySslSOAPBinding
32                                           
33from ndg.security.common.utils import VettedDict, str2Bool
34from ndg.security.common.credentialwallet import SAMLAssertionWallet
35
36
37class SessionCache(object): 
38    """Class to cache previous attribute query results retrieved from
39    Attribute Authority callouts.  This is to optimise performance.  Session
40    caching is based on beaker.session
41   
42    @ivar __session: wrapped beaker session instance
43    @type __session: beaker.session.Session
44    """
45    __slots__ = ('__session', )
46   
47    def __init__(self, _id, data_dir=None, timeout=None):
48        """
49        @param _id: unique identifier for session to be created, or one to reload
50        from store
51        @type _id: basestring
52        @param data_dir: directory for permanent storage of sessions.
53        Sessions are used as a means of optimisation caching Attribute Query
54        results to reduce the number of Attribute Authority web service calls.
55        If set to None, sessions are cached in memory only.
56        @type data_dir: None type / basestring
57        @param timeout: time in seconds for individual caches' lifetimes.  Set
58        to None to set no expiry.
59        @type timeout: float/int/long or None type
60        """               
61        # Expecting URIs for Ids, make them safe for storage by encoding first
62        encodedId = base64.b64encode(_id)
63       
64        # The first argument is the request object, a dictionary-like object
65        # from which and to which cookie settings are made.  This can be ignored
66        # here as the cookie functionality is not being used.
67        self.__session = beaker.session.Session({}, id=encodedId, 
68                                                data_dir=data_dir,
69                                                timeout=timeout,
70                                                use_cookies=False)
71        if 'wallet' not in self.__session:
72            self.__session['wallet'] = SAMLAssertionWallet()
73        else:
74            # Prune expired assertions
75            self.__session['wallet'].audit()
76   
77    def add(self, assertions, issuerEndpoint):
78        """Add a SAML assertion containing attribute statement(s) from an
79        Attribute Authority
80       
81        @type assertions: ndg.security.common.utils.TypedList
82        @param assertions: new SAML assertions to be added corresponding to the
83        issuerEndpoint
84        @type issuerEndpoint: basestring
85        @param issuerEndpoint: input the issuing service URI from
86        which assertions were retrieved.  This is added to a dict to enable
87        access to given Assertions keyed by issuing service URI. See the
88        retrieveAssertions method.
89        @raise KeyError: error with session object - no wallet key set
90        """
91        self.__session['wallet'].addCredentials(issuerEndpoint, assertions)
92       
93    def retrieve(self, issuerEndpoint):
94        '''Get the cached assertions for the given Attribute Authority issuer
95       
96        @type issuerEndpoint: basestring
97        @param issuerEndpoint: input the issuing service URI from
98        which assertion was retrieved.
99        @return: SAML assertion response cached from a previous call to the
100        Attribute Authority with the given endpoint
101        @raise KeyError: error with session object - no wallet key set
102        '''
103        wallet = self.__session['wallet']
104        return wallet.retrieveCredentials(issuerEndpoint)
105           
106    def __del__(self):
107        """Ensure session is saved when this object goes out of scope"""
108        if isinstance(self.__session, beaker.session.Session):
109            self.__session.save()
110       
111
112class PIPException(Exception):
113    """Base exception type for XACML PIP (Policy Information Point) class"""
114   
115   
116class PIPConfigException(PIPException):
117    """Configuration errors related to the XACML PIP (Policy Information Point)
118    class
119    """
120
121
122class PIPRequestCtxException(PIPException):
123    """Error with request context passed to XACML PIP object's attribute query
124    """
125   
126   
127class PIP(PIPInterface):
128    '''Policy Information Point enables XACML PDP to query for additional user
129    attributes.  The PDP does this indirectly via the Context Handler   
130    '''
131    # Subject attributes makes no sense for external configuration - these
132    # are set at run time based on the given subject identity
133    DISALLOWED_ATTRIBUTE_QUERY_OPTNAMES = (
134        AttributeQuerySslSOAPBinding.SUBJECT_ID_OPTNAME,
135        AttributeQuerySslSOAPBinding.QUERY_ATTRIBUTES_ATTRNAME
136    )
137   
138    # Special attribute setting for SAML Attribute Query attributes - see
139    # __setattr__
140    ATTRIBUTE_QUERY_ATTRNAME = 'attributeQuery'
141    LEN_ATTRIBUTE_QUERY_ATTRNAME = len(ATTRIBUTE_QUERY_ATTRNAME)
142   
143    # +1 allows for '.' or other separator e.g.
144    # pip.attributeQuery.issuerName
145    #                   ^
146    ATTRIBUTE_QUERY_ATTRNAME_OFFSET = LEN_ATTRIBUTE_QUERY_ATTRNAME + 1
147   
148    DEFAULT_OPT_PREFIX = 'saml_pip.'
149
150    XACML_ATTR_VAL_CLASS_FACTORY = XacmlAttributeValueClassFactory()
151   
152    __slots__ = (
153        '__subjectAttributeId',
154        '__mappingFilePath', 
155        '__attributeId2AttributeAuthorityMap',
156        '__attributeQueryBinding',
157        '__cacheSessions',
158        '__sessionCacheDataDir',
159        '__sessionCacheTimeout',
160        '__sessionCache'
161    )
162   
163    def __init__(self, sessionCacheDataDir=None, sessionCacheTimeout=None):
164        '''Initialise settings for connection to an Attribute Authority
165       
166        @param sessionCacheDataDir: directory for permanent storage of sessions.
167        Sessions are used as a means of optimisation caching Attribute Query
168        results to reduce the number of Attribute Authority web service calls.
169        If set to None, sessions are cached in memory only.
170        @type sessionCacheDataDir: None type / basestring
171        @param sessionCacheTimeout: time in seconds for individual caches'
172        lifetimes.  Set to None to set no expiry.
173        @type sessionCacheTimeout: float/int/long/string or None type
174        '''
175        self.sessionCacheDataDir = sessionCacheDataDir
176        self.sessionCacheTimeout = sessionCacheTimeout
177       
178        self.__subjectAttributeId = None
179        self.__mappingFilePath = None
180       
181        # Force mapping dict to have string type keys and items
182        _typeCheckers = (lambda val: isinstance(val, basestring),)*2
183        self.__attributeId2AttributeAuthorityMap = VettedDict(*_typeCheckers)
184       
185        self.__attributeQueryBinding = AttributeQuerySslSOAPBinding()
186       
187        self.__cacheSessions = True
188        self.__sessionCache = None
189
190    def _getSessionCacheTimeout(self):
191        return self.__sessionCacheTimeout
192
193    def _setSessionCacheTimeout(self, value):
194        if value is None:
195            self.__sessionCacheTimeout = value
196         
197        elif isinstance(value, basestring):
198            self.__sessionCacheTimeout = float(value)
199             
200        elif isinstance(value, (int, float, long)):
201            self.__sessionCacheTimeout = value
202           
203        else:
204            raise TypeError('Expecting None, float, int, long or string type; '
205                            'got %r' % type(value))
206
207    sessionCacheTimeout = property(_getSessionCacheTimeout, 
208                                   _setSessionCacheTimeout, 
209                                   doc='Set individual session caches to '
210                                       'timeout after this period (seconds).  '
211                                       'Set to None to have no timeout')
212
213    def _getCacheSessions(self):
214        return self.__cacheSessions
215
216    def _setCacheSessions(self, value):
217        if isinstance(value, basestring):
218            self.__cacheSessions = str2Bool(value)
219        elif isinstance(value, bool):
220            self.__cacheSessions = value
221        else:
222            raise TypeError('Expecting string/bool type for "cacheSessions" '
223                            'attribute; got %r' % type(value))
224       
225        self.__cacheSessions = value
226
227    cacheSessions = property(_getCacheSessions, _setCacheSessions, 
228                             doc="Cache attribute query results to optimise "
229                                 "performance")
230
231    def _getSessionCacheDataDir(self):
232        return self.__sessionCacheDataDir
233
234    def _setSessionCacheDataDir(self, value):
235        if not isinstance(value, (basestring, type(None))):
236            raise TypeError('Expecting string/None type for '
237                            '"sessionCacheDataDir"; got %r' % type(value))
238           
239        self.__sessionCacheDataDir = value
240
241    sessionCacheDataDir = property(_getSessionCacheDataDir, 
242                                   _setSessionCacheDataDir, 
243                                   doc="Data Directory for Session Cache.  "
244                                       "This setting will be ignored if "
245                                       '"cacheSessions" is set to False')
246   
247    def _get_subjectAttributeId(self):
248        return self.__subjectAttributeId
249
250    def _set_subjectAttributeId(self, value):
251        if not isinstance(value, basestring):
252            raise TypeError('Expecting string type for "subjectAttributeId"; '
253                            'got %r' % type(value))
254        self.__subjectAttributeId = value
255
256    subjectAttributeId = property(_get_subjectAttributeId, 
257                                  _set_subjectAttributeId,
258                                  doc="The attribute ID of the subject value "
259                                      "to extract from the XACML request "
260                                      "context and pass in the SAML attribute "
261                                      "query")
262                                       
263    def _getMappingFilePath(self):
264        return self.__mappingFilePath
265
266    def _setMappingFilePath(self, value):
267        if not isinstance(value, basestring):
268            raise TypeError('Expecting string type for "mappingFilePath"; got '
269                            '%r' % type(value))
270        self.__mappingFilePath = path.expandvars(value)
271
272    mappingFilePath = property(_getMappingFilePath, 
273                               _setMappingFilePath, 
274                               doc="Mapping File maps Attribute ID -> "
275"Attribute Authority mapping file.  The PIP, on receipt of a query from the "
276"XACML context handler, checks the attribute(s) being queried for and looks up "
277"this mapping to determine which attribute authority to query to find out if "
278"the subject has the attribute in their entitlement.")
279   
280    attribute2AttributeAuthorityMap = property(
281                    fget=lambda self: self.__attributeId2AttributeAuthorityMap,
282                    doc="Mapping from attribute Id to attribute authority "
283                        "endpoint")
284   
285    @property
286    def attributeQueryBinding(self):
287        """SAML SOAP Attribute Query client binding object"""
288        return self.__attributeQueryBinding
289   
290    @classmethod
291    def fromConfig(cls, cfg, **kw):
292        '''Alternative constructor makes object from config file settings
293        @type cfg: basestring /ConfigParser derived type
294        @param cfg: configuration file path or ConfigParser type object
295        @rtype: ndg.security.server.xacml.pip.saml_pip.PIP
296        @return: new instance of this class
297        '''
298        obj = cls()
299        obj.parseConfig(cfg, **kw)
300       
301        return obj
302
303    def parseConfig(self, cfg, prefix=DEFAULT_OPT_PREFIX, section='DEFAULT'):
304        '''Read config settings from a file, config parser object or dict
305       
306        @type cfg: basestring / ConfigParser derived type / dict
307        @param cfg: configuration file path or ConfigParser type object
308        @type prefix: basestring
309        @param prefix: prefix for option names e.g. "attributeQuery."
310        @type section: basetring
311        @param section: configuration file section from which to extract
312        parameters.
313        ''' 
314        if isinstance(cfg, basestring):
315            cfgFilePath = path.expandvars(cfg)
316           
317            # Add a 'here' helper option for setting dir paths in the config
318            # file
319            hereDir = path.abspath(path.dirname(cfgFilePath))
320            _cfg = SafeConfigParser(defaults={'here': hereDir})
321           
322            # Make option name reading case sensitive
323            _cfg.optionxform = str
324            _cfg.read(cfgFilePath)
325            items = _cfg.items(section)
326           
327        elif isinstance(cfg, ConfigParser):
328            items = cfg.items(section)
329         
330        elif isinstance(cfg, dict):
331            items = cfg.items()     
332        else:
333            raise AttributeError('Expecting basestring, ConfigParser or dict '
334                                 'type for "cfg" attribute; got %r type' % 
335                                 type(cfg))
336       
337        prefixLen = len(prefix)
338       
339        for optName, val in items:
340            if prefix:
341                # Filter attributes based on prefix
342                if optName.startswith(prefix):
343                    setattr(self, optName[prefixLen:], val)
344            else:
345                # No prefix set - attempt to set all attributes   
346                setattr(self, optName, val)
347                           
348    def __setattr__(self, name, value):
349        """Enable setting of AttributeQuerySslSOAPBinding attributes from
350        names starting with attributeQuery.* / attributeQuery_*.  Addition for
351        setting these values from ini file
352        """
353
354        # Coerce into setting AttributeQuerySslSOAPBinding attributes -
355        # names must start with 'attributeQuery\W' e.g.
356        # attributeQuery.clockSkewTolerance or attributeQuery_issuerDN
357        if name.startswith(self.__class__.ATTRIBUTE_QUERY_ATTRNAME):
358            queryAttrName = name[
359                                self.__class__.ATTRIBUTE_QUERY_ATTRNAME_OFFSET:]
360           
361            # Skip subject related parameters to prevent settings from static
362            # configuration.  These are set at runtime
363            if min([queryAttrName.startswith(i) 
364                    for i in self.__class__.DISALLOWED_ATTRIBUTE_QUERY_OPTNAMES
365                    ]):
366                super(PIP, self).__setattr__(name, value)
367               
368            setattr(self.__attributeQueryBinding, queryAttrName, value)           
369        else:
370            super(PIP, self).__setattr__(name, value)
371   
372    def readMappingFile(self):
373        """Read the file which maps attribute names to Attribute Authorities
374        """
375        mappingFile = open(self.mappingFilePath)
376        for line in mappingFile.readlines():
377            _line = path.expandvars(line).strip()
378            if _line and not _line.startswith('#'):
379                attributeId, attributeAuthorityURI = _line.split()
380                self.__attributeId2AttributeAuthorityMap[attributeId
381                                                       ] = attributeAuthorityURI
382       
383    def attributeQuery(self, context, attributeDesignator):
384        """Query this PIP for the given request context attribute specified by
385        the attribute designator.  Nb. this implementation is only intended to
386        accept queries for a given *subject* in the request
387       
388        @param context: the request context
389        @type context: ndg.xacml.core.context.request.Request
390        @param designator:
391        @type designator: ndg.xacml.core.attributedesignator.SubjectAttributeDesignator
392        @rtype: ndg.xacml.utils.TypedList(<attributeDesignator.dataType>) / None
393        @return: attribute values found for query subject or None if none
394        could be found
395        @raise PIPConfigException: if attribute ID -> Attribute Authority
396        mapping is empty 
397        """
398       
399        # Check the attribute designator type - this implementation takes
400        # queries for request context subjects only
401        if not isinstance(attributeDesignator, SubjectAttributeDesignator):
402            log.debug('This PIP query interface can only accept subject '
403                      'attribute designator related queries')
404            return None
405       
406        if not isinstance(context, XacmlRequestCtx):
407            raise TypeError('Expecting %r type for context input; got %r' %
408                            (XacmlRequestCtx, type(context)))
409       
410        # Look up mapping from request attribute ID to Attribute Authority to
411        # query
412        if len(self.__attributeId2AttributeAuthorityMap) == 0:
413            raise PIPConfigException('No entries found in attribute ID to '
414                                     'Attribute Authority mapping')
415           
416        attributeAuthorityURI = self.__attributeId2AttributeAuthorityMap.get(
417                                            attributeDesignator.attributeId,
418                                            None)
419        if attributeAuthorityURI is None:
420            log.debug("No matching attribute authority endpoint found in "
421                      "mapping file %r for input attribute ID %r", 
422                      self.mappingFilePath,
423                      attributeDesignator.attributeId)
424           
425            return None
426       
427        # Get subject from the request context
428        subject = None
429        subjectId = None
430        for subject in context.subjects:
431            for attribute in subject.attributes:
432                if attribute.attributeId == self.subjectAttributeId:
433                    if len(attribute.attributeValues) != 1:
434                        raise PIPRequestCtxException("Expecting a single "
435                                                     "attribute value "
436                                                     "for query subject ID")
437                    subjectId = attribute.attributeValues[0].value
438                    break
439       
440        if subjectId is None:
441            raise PIPRequestCtxException('No subject found of type %r in '
442                                         'request context' %
443                                         self.subjectAttributeId)
444        elif not subjectId:
445            # Empty string
446            return None
447        else:
448            # Keep a reference to the matching Subject instance
449            xacmlCtxSubject = subject
450           
451        attributeFormat = attributeDesignator.dataType
452        attributeId = attributeDesignator.attributeId
453           
454        # Check for cached attributes for this subject (i.e. user)       
455        # If none found send a query to the attribute authority
456        if self.cacheSessions:
457            sessionCache = SessionCache(subjectId,
458                                        data_dir=self.__sessionCacheDataDir,
459                                        timeout=self.__sessionCacheTimeout)
460            assertions = sessionCache.retrieve(attributeAuthorityURI)
461        else:
462            assertions = None
463           
464        if assertions is None:
465            # No cached assertions are available for this Attribute Authority,
466            # make a fresh call
467           
468            # Get the id of the attribute to be queried for and add it to the
469            # SAML query
470           
471            samlAttribute = SamlAttribute()
472            samlAttribute.name = attributeDesignator.attributeId
473            samlAttribute.nameFormat = attributeFormat
474            self.attributeQueryBinding.query.attributes.append(samlAttribute)
475           
476            # Dispatch query
477            try:
478                self.attributeQueryBinding.subjectID = subjectId
479                self.attributeQueryBinding.subjectIdFormat = \
480                                                    self.subjectAttributeId
481                response = self.attributeQueryBinding.send(
482                                                    uri=attributeAuthorityURI)
483            except Exception:
484                log.exception('Error querying Attribute service %r with '
485                              'subject %r', attributeAuthorityURI, subjectId)
486                raise
487            finally:
488                # !Ensure relevant query attributes are reset ready for any
489                # subsequent query!
490                self.attributeQueryBinding.subjectID = ''
491                self.attributeQueryBinding.subjectIdFormat = ''
492                self.attributeQueryBinding.query.attributes = SamlTypedList(
493                                                                SamlAttribute)
494       
495            assertions = response.assertions
496            if self.cacheSessions:
497                sessionCache.add(assertions, attributeAuthorityURI)
498           
499        # Unpack SAML assertion attribute corresponding to the name
500        # format specified and copy into XACML attributes     
501        xacmlAttribute = XacmlAttribute()
502        xacmlAttribute.attributeId = attributeId
503        xacmlAttribute.dataType = attributeFormat
504       
505        # Create XACML class from SAML type identifier
506        factory = self.__class__.XACML_ATTR_VAL_CLASS_FACTORY
507        xacmlAttrValClass = factory(attributeFormat)
508       
509        for assertion in assertions:
510            for statement in assertion.attributeStatements:
511                for attribute in statement.attributes:
512                    if attribute.nameFormat == attributeFormat:
513                        # Convert SAML Attribute values to XACML equivalent
514                        # types
515                        for samlAttrVal in attribute.attributeValues: 
516                            # Instantiate and initial new XACML value
517                            xacmlAttrVal = xacmlAttrValClass(
518                                                        value=samlAttrVal.value)
519                           
520                            xacmlAttribute.attributeValues.append(xacmlAttrVal)
521       
522        # Update the XACML request context subject with the new attributes
523        matchFound = False
524        for attr in xacmlCtxSubject.attributes:
525            matchFound = attr.attributeId == attributeId
526            if matchFound:
527                # Weed out duplicates
528                newAttrVals = [attrVal
529                               for attrVal in xacmlAttribute.attributeValues
530                               if attrVal not in attr.attributeValues]
531                attr.attributeValues.extend(newAttrVals)
532                break
533           
534        if not matchFound:
535            xacmlCtxSubject.attributes.append(xacmlAttribute)
536       
537        # Return the attributes to the caller to comply with the interface
538        return xacmlAttribute.attributeValues
Note: See TracBrowser for help on using the repository browser.