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

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