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

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@7357
Revision 7357, 15.6 KB checked in by pjkersha, 9 years ago (diff)

Incomplete - task 2: XACML-Security Integration

  • implemented caching of authorisation decision statements in the PEP to cut down on calls to authorisation service.
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
16
17from ndg.xacml.core.attributedesignator import SubjectAttributeDesignator
18from ndg.xacml.core.attribute import Attribute as XacmlAttribute
19from ndg.xacml.core.attributevalue import AttributeValueClassFactory as \
20    XacmlAttributeValueClassFactory
21from ndg.xacml.core.context.pipinterface import PIPInterface
22from ndg.xacml.core.context.request import Request as XacmlRequestCtx
23
24from ndg.saml.saml2.core import (AttributeQuery as SamlAttributeQuery, 
25                                 Attribute as SamlAttribute)
26from ndg.saml.utils import TypedList as SamlTypedList
27from ndg.saml.saml2.binding.soap.client.attributequery import \
28                                            AttributeQuerySslSOAPBinding
29                                           
30from ndg.security.common.utils import VettedDict
31
32
33class PIPException(Exception):
34    """Base exception type for XACML PIP (Policy Information Point) class"""
35   
36   
37class PIPConfigException(PIPException):
38    """Configuration errors related to the XACML PIP (Policy Information Point)
39    class
40    """
41
42
43class PIPRequestCtxException(PIPException):
44    """Error with request context passed to XACML PIP object's attribute query
45    """
46   
47   
48class PIP(PIPInterface):
49    '''Policy Information Point enables XACML PDP to query for additional user
50    attributes.  The PDP does this indirectly via the Context Handler   
51    '''
52    # Subject attributes makes no sense for external configuration - these
53    # are set at run time based on the given subject identity
54    DISALLOWED_ATTRIBUTE_QUERY_OPTNAMES = (
55        AttributeQuerySslSOAPBinding.SUBJECT_ID_OPTNAME,
56        AttributeQuerySslSOAPBinding.QUERY_ATTRIBUTES_ATTRNAME
57    )
58    ATTRIBUTE_QUERY_ATTRNAME = 'attributeQuery'
59    LEN_ATTRIBUTE_QUERY_ATTRNAME = len(ATTRIBUTE_QUERY_ATTRNAME)
60   
61    # +1 allows for '.' or other separator e.g.
62    # pip.attributeQuery.issuerName
63    #                   ^
64    ATTRIBUTE_QUERY_ATTRNAME_OFFSET = LEN_ATTRIBUTE_QUERY_ATTRNAME + 1
65   
66    DEFAULT_OPT_PREFIX = 'saml_pip.'
67
68    XACML_ATTR_VAL_CLASS_FACTORY = XacmlAttributeValueClassFactory()
69   
70    __slots__ = (
71        '__subjectAttributeId',
72        '__mappingFilePath', 
73        '__attributeId2AttributeAuthorityMap',
74        '__attributeQueryBinding'
75    )
76   
77    def __init__(self):
78        '''Initialise settings for connection to an Attribute Authority'''
79        self.__subjectAttributeId = None
80        self.__mappingFilePath = None
81       
82        # Force mapping dict to have string type keys and items
83        _typeCheckers = (lambda val: isinstance(val, basestring),)*2
84        self.__attributeId2AttributeAuthorityMap = VettedDict(*_typeCheckers)
85       
86        self.__attributeQueryBinding = AttributeQuerySslSOAPBinding()
87       
88    def _get_subjectAttributeId(self):
89        return self.__subjectAttributeId
90
91    def _set_subjectAttributeId(self, value):
92        if not isinstance(value, basestring):
93            raise TypeError('Expecting string type for "subjectAttributeId"; '
94                            'got %r' % type(value))
95        self.__subjectAttributeId = value
96
97    subjectAttributeId = property(_get_subjectAttributeId, 
98                                  _set_subjectAttributeId,
99                                  doc="The attribute ID of the subject value "
100                                      "to extract from the XACML request "
101                                      "context and pass in the SAML attribute "
102                                      "query")
103                                       
104    def _getMappingFilePath(self):
105        return self.__mappingFilePath
106
107    def _setMappingFilePath(self, value):
108        if not isinstance(value, basestring):
109            raise TypeError('Expecting string type for "mappingFilePath"; got '
110                            '%r' % type(value))
111        self.__mappingFilePath = path.expandvars(value)
112
113    mappingFilePath = property(_getMappingFilePath, 
114                               _setMappingFilePath, 
115                               doc="Mapping File maps Attribute ID -> "
116"Attribute Authority mapping file.  The PIP, on receipt of a query from the "
117"XACML context handler, checks the attribute(s) being queried for and looks up "
118"this mapping to determine which attribute authority to query to find out if "
119"the subject has the attribute in their entitlement.")
120   
121    attribute2AttributeAuthorityMap = property(
122                    fget=lambda self: self.__attributeId2AttributeAuthorityMap,
123                    doc="Mapping from attribute Id to attribute authority "
124                        "endpoint")
125   
126    @property
127    def attributeQueryBinding(self):
128        """SAML SOAP Attribute Query client binding object"""
129        return self.__attributeQueryBinding
130   
131    @classmethod
132    def fromConfig(cls, cfg, **kw):
133        '''Alternative constructor makes object from config file settings
134        @type cfg: basestring /ConfigParser derived type
135        @param cfg: configuration file path or ConfigParser type object
136        @rtype: ndg.security.server.xacml.pip.saml_pip.PIP
137        @return: new instance of this class
138        '''
139        obj = cls()
140        obj.parseConfig(cfg, **kw)
141       
142        return obj
143
144    def parseConfig(self, cfg, prefix=DEFAULT_OPT_PREFIX, section='DEFAULT'):
145        '''Read config settings from a file, config parser object or dict
146       
147        @type cfg: basestring / ConfigParser derived type / dict
148        @param cfg: configuration file path or ConfigParser type object
149        @type prefix: basestring
150        @param prefix: prefix for option names e.g. "attributeQuery."
151        @type section: basetring
152        @param section: configuration file section from which to extract
153        parameters.
154        ''' 
155        if isinstance(cfg, basestring):
156            cfgFilePath = path.expandvars(cfg)
157           
158            # Add a 'here' helper option for setting dir paths in the config
159            # file
160            hereDir = path.abspath(path.dirname(cfgFilePath))
161            _cfg = SafeConfigParser(defaults={'here': hereDir})
162           
163            # Make option name reading case sensitive
164            _cfg.optionxform = str
165            _cfg.read(cfgFilePath)
166            items = _cfg.items(section)
167           
168        elif isinstance(cfg, ConfigParser):
169            items = cfg.items(section)
170         
171        elif isinstance(cfg, dict):
172            items = cfg.items()     
173        else:
174            raise AttributeError('Expecting basestring, ConfigParser or dict '
175                                 'type for "cfg" attribute; got %r type' % 
176                                 type(cfg))
177       
178        prefixLen = len(prefix)
179       
180        for optName, val in items:
181            if prefix:
182                # Filter attributes based on prefix
183                if optName.startswith(prefix):
184                    setattr(self, optName[prefixLen:], val)
185            else:
186                # No prefix set - attempt to set all attributes   
187                setattr(self, optName, val)
188                           
189    def __setattr__(self, name, value):
190        """Enable setting of AttributeQuerySslSOAPBinding attributes from
191        names starting with attributeQuery.* / attributeQuery_*.  Addition for
192        setting these values from ini file
193        """
194
195        # Coerce into setting AttributeQuerySslSOAPBinding attributes -
196        # names must start with 'attributeQuery\W' e.g.
197        # attributeQuery.clockSkewTolerance or attributeQuery_issuerDN
198        if name.startswith(self.__class__.ATTRIBUTE_QUERY_ATTRNAME):
199            queryAttrName = name[
200                                self.__class__.ATTRIBUTE_QUERY_ATTRNAME_OFFSET:]
201           
202            # Skip subject related parameters to prevent settings from static
203            # configuration.  These are set at runtime
204            if min([queryAttrName.startswith(i) 
205                    for i in self.__class__.DISALLOWED_ATTRIBUTE_QUERY_OPTNAMES
206                    ]):
207                super(PIP, self).__setattr__(name, value)
208               
209            setattr(self.__attributeQueryBinding, queryAttrName, value)
210        else:
211            super(PIP, self).__setattr__(name, value)
212   
213    def readMappingFile(self):
214        """Read the file which maps attribute names to Attribute Authorities
215        """
216        mappingFile = open(self.mappingFilePath)
217        for line in mappingFile.readlines():
218            _line = path.expandvars(line).strip()
219            if _line and not _line.startswith('#'):
220                attributeId, attributeAuthorityURI = _line.split()
221                self.__attributeId2AttributeAuthorityMap[attributeId
222                                                       ] = attributeAuthorityURI
223       
224    def attributeQuery(self, context, attributeDesignator):
225        """Query this PIP for the given request context attribute specified by
226        the attribute designator.  Nb. this implementation is only intended to
227        accept queries for a given *subject* in the request
228       
229        @param context: the request context
230        @type context: ndg.xacml.core.context.request.Request
231        @param designator:
232        @type designator: ndg.xacml.core.attributedesignator.SubjectAttributeDesignator
233        @rtype: ndg.xacml.utils.TypedList(<attributeDesignator.dataType>) / None
234        @return: attribute values found for query subject or None if none
235        could be found
236        @raise PIPConfigException: if attribute ID -> Attribute Authority
237        mapping is empty 
238        """
239       
240        # Check the attribute designator type - this implementation takes
241        # queries for request context subjects only
242        if not isinstance(attributeDesignator, SubjectAttributeDesignator):
243            log.debug('This PIP query interface can only accept subject '
244                      'attribute designator related queries')
245            return None
246       
247        if not isinstance(context, XacmlRequestCtx):
248            raise TypeError('Expecting %r type for context input; got %r' %
249                            (XacmlRequestCtx, type(context)))
250       
251        # TODO: Check for cached attributes for this subject (i.e. user)       
252        # If none found send a query to the attribute authority
253
254        # Look up mapping from request attribute ID to Attribute Authority to
255        # query
256        if len(self.__attributeId2AttributeAuthorityMap) == 0:
257            raise PIPConfigException('No entries found in attribute ID to '
258                                     'Attribute Authority mapping')
259           
260        attributeAuthorityURI = self.__attributeId2AttributeAuthorityMap.get(
261                                            attributeDesignator.attributeId,
262                                            None)
263        if attributeAuthorityURI is None:
264            log.debug("No matching attribute authority endpoint found in "
265                      "mapping file %r for input attribute ID %r", 
266                      self.mappingFilePath,
267                      attributeDesignator.attributeId)
268           
269            return None
270       
271        # Get subject from the request context
272        subjectId = None
273        for subject in context.subjects:
274            for attribute in subject.attributes:
275                if attribute.attributeId == self.subjectAttributeId:
276                    if len(attribute.attributeValues) != 1:
277                        raise PIPRequestCtxException("Expecting a single "
278                                                     "attribute value "
279                                                     "for query subject ID")
280                    subjectId = attribute.attributeValues[0].value
281                    break
282       
283        if subjectId is None:
284            raise PIPRequestCtxException('No subject found of type %r in '
285                                         'request context' %
286                                         self.subjectAttributeId)
287        elif not subjectId:
288            # Empty string
289            return None
290        else:
291            # Keep a reference to the matching Subject instance
292            xacmlCtxSubject = subject
293           
294        # Get the id of the attribute to be queried for and add it to the SAML
295        # query
296        attributeFormat = attributeDesignator.dataType
297        attributeId = attributeDesignator.attributeId
298       
299        samlAttribute = SamlAttribute()
300        samlAttribute.name = attributeDesignator.attributeId
301        samlAttribute.nameFormat = attributeFormat
302        self.attributeQueryBinding.query.attributes.append(samlAttribute)
303       
304        # Dispatch query
305        try:
306            self.attributeQueryBinding.subjectID = subjectId
307            self.attributeQueryBinding.subjectIdFormat = self.subjectAttributeId
308            response = self.attributeQueryBinding.send(
309                                                    uri=attributeAuthorityURI)
310        except Exception:
311            log.exception('Error querying Attribute service %r with subject %r',
312                          attributeAuthorityURI,
313                          subjectId)
314            raise
315        finally:
316            # !Ensure relevant query attributes are reset ready for any
317            # subsequent query!
318            self.attributeQueryBinding.subjectID = ''
319            self.attributeQueryBinding.subjectIdFormat = ''
320            self.attributeQueryBinding.query.attributes = SamlTypedList(
321                                                                SamlAttribute)
322       
323        # Unpack SAML assertion attribute corresponding to the name
324        # format specified and copy into XACML attributes     
325        xacmlAttribute = XacmlAttribute()
326        xacmlAttribute.attributeId = attributeId
327        xacmlAttribute.dataType = attributeFormat
328       
329        # Create XACML class from SAML type identifier
330        factory = self.__class__.XACML_ATTR_VAL_CLASS_FACTORY
331        xacmlAttrValClass = factory(attributeFormat)
332       
333        for assertion in response.assertions:
334            for statement in assertion.attributeStatements:
335                for attribute in statement.attributes:
336                    if attribute.nameFormat == attributeFormat:
337                        # Convert SAML Attribute values to XACML equivalent
338                        # types
339                        for samlAttrVal in attribute.attributeValues: 
340                            # Instantiate and initial new XACML value
341                            xacmlAttrVal = xacmlAttrValClass(
342                                                        value=samlAttrVal.value)
343                           
344                            xacmlAttribute.attributeValues.append(xacmlAttrVal)
345       
346        # Update the XACML request context subject with the new attributes
347        matchFound = False
348        for attr in xacmlCtxSubject.attributes:
349            matchFound = attr.attributeId == attributeId
350            if matchFound:
351                newAttrVals = [attrVal
352                               for attrVal in xacmlAttribute.attributeValues
353                               if attrVal not in attr.attributeValues]
354                attr.attributeValues.extend(newAttrVals)
355                break
356           
357        if not matchFound:
358            xacmlCtxSubject.attributes.append(xacmlAttribute)
359       
360        # Return the attributes to the caller to comply with the interface
361        return xacmlAttribute.attributeValues
362       
Note: See TracBrowser for help on using the repository browser.