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

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@7350
Revision 7350, 15.0 KB checked in by pjkersha, 10 years ago (diff)

Incomplete - task 2: XACML-Security Integration

  • Working version integrated with the ndg.security.test.integration.full_system test. This secures a test HTTP app with the XACML based authorisation called over a SAML interface from a PEP in the app's authorisation middleware
  • Some tuning is needed to optimise performance:
    • caching of attribute queries in the PEP
    • Possible additional PDP in the authorisation filter to filter out some requests from being routed to the SAML authorisation service.
    • possible caching of authorisation decisions at the PEP as another way of avoiding the authorisation service round-trips.
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 mapping is
237        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 Exception("Expecting a single attribute value "
278                                        "for query subject ID")
279                    subjectId = attribute.attributeValues[0].value
280                    break
281       
282        if subjectId is None:
283            raise PIPRequestCtxException('No subject found of type %r in '
284                                         'request context' %
285                                         self.subjectAttributeId)
286        elif not subjectId:
287            # Empty string
288            return None
289        else:
290            # Keep a reference to the matching Subject instance
291            xacmlCtxSubject = subject
292           
293        # Get the id of the attribute to be queried for and add it to the SAML
294        # query
295        attributeFormat = attributeDesignator.dataType
296        samlAttribute = SamlAttribute()
297        samlAttribute.name = attributeDesignator.attributeId
298        samlAttribute.nameFormat = attributeFormat
299        self.attributeQueryBinding.query.attributes.append(samlAttribute)
300       
301        # Dispatch query
302        try:
303            self.attributeQueryBinding.subjectID = subjectId
304            self.attributeQueryBinding.subjectIdFormat = self.subjectAttributeId
305            response = self.attributeQueryBinding.send(
306                                                    uri=attributeAuthorityURI)
307        except Exception:
308            log.exception('Error querying Attribute service %r with subject %r',
309                          attributeAuthorityURI,
310                          subjectId)
311            raise
312        finally:
313            # !Ensure relevant query attributes are reset ready for any
314            # subsequent query!
315            self.attributeQueryBinding.subjectID = ''
316            self.attributeQueryBinding.subjectIdFormat = ''
317            self.attributeQueryBinding.query.attributes = SamlTypedList(
318                                                                SamlAttribute)
319       
320        # Unpack SAML assertion attribute corresponding to the name
321        # format specified and copy into XACML attributes     
322        xacmlAttribute = XacmlAttribute()
323        xacmlAttribute.attributeId = attributeDesignator.attributeId
324        xacmlAttribute.dataType = attributeFormat
325       
326        # Create XACML class from SAML type identifier
327        factory = self.__class__.XACML_ATTR_VAL_CLASS_FACTORY
328        xacmlAttrValClass = factory(attributeFormat)
329       
330        for assertion in response.assertions:
331            for statement in assertion.attributeStatements:
332                for attribute in statement.attributes:
333                    if attribute.nameFormat == attributeFormat:
334                        # Convert SAML Attribute values to XACML equivalent
335                        # types
336                        for samlAttrVal in attribute.attributeValues: 
337                            # Instantiate and initial new XACML value
338                            xacmlAttrVal = xacmlAttrValClass(
339                                                        value=samlAttrVal.value)
340                           
341                            xacmlAttribute.attributeValues.append(xacmlAttrVal)
342       
343        # Update the XACML request context subject with the new attributes
344        xacmlCtxSubject.attributes.append(xacmlAttribute)
345       
346        # Return the attributes to the caller to comply with the interface
347        return xacmlAttribute.attributeValues
348       
Note: See TracBrowser for help on using the repository browser.