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

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