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

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

Incomplete - task 2: XACML-Security Integration

  • first working e2e test with PEP calling a SAML Authorisation service configured with PIP to make callouts to an Attribute Authority to pull user attributes. This meets the ESG requirements. Next steps:
    • integrate with ndg.security.test.integration.authz_lite browser based integration tests
    • optimise by adding caching of authz decisions to PEP and possibly caching attribute assertions in the PEP.
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        else:
287            # Keep a reference to the matching Subject instance
288            xacmlCtxSubject = subject
289           
290        # Get the id of the attribute to be queried for and add it to the SAML
291        # query
292        attributeFormat = attributeDesignator.dataType
293        samlAttribute = SamlAttribute()
294        samlAttribute.name = attributeDesignator.attributeId
295        samlAttribute.nameFormat = attributeFormat
296        self.attributeQueryBinding.query.attributes.append(samlAttribute)
297       
298        # Dispatch query
299        try:
300            self.attributeQueryBinding.subjectID = subjectId
301            self.attributeQueryBinding.subjectIdFormat = self.subjectAttributeId
302            response = self.attributeQueryBinding.send(
303                                                    uri=attributeAuthorityURI)
304        except Exception:
305            log.exception('Error querying Attribute service %r with subject %r',
306                          attributeAuthorityURI,
307                          subjectId)
308            raise
309        finally:
310            # !Ensure relevant query attributes are reset ready for any
311            # subsequent query!
312            self.attributeQueryBinding.subjectID = ''
313            self.attributeQueryBinding.subjectIdFormat = ''
314            self.attributeQueryBinding.query.attributes = SamlTypedList(
315                                                                SamlAttribute)
316       
317        # Unpack SAML assertion attribute corresponding to the name
318        # format specified and copy into XACML attributes     
319        xacmlAttribute = XacmlAttribute()
320        xacmlAttribute.attributeId = attributeDesignator.attributeId
321        xacmlAttribute.dataType = attributeFormat
322       
323        # Create XACML class from SAML type identifier
324        factory = self.__class__.XACML_ATTR_VAL_CLASS_FACTORY
325        xacmlAttrValClass = factory(attributeFormat)
326       
327        for assertion in response.assertions:
328            for statement in assertion.attributeStatements:
329                for attribute in statement.attributes:
330                    if attribute.nameFormat == attributeFormat:
331                        # Convert SAML Attribute values to XACML equivalent
332                        # types
333                        for samlAttrVal in attribute.attributeValues: 
334                            # Instantiate and initial new XACML value
335                            xacmlAttrVal = xacmlAttrValClass(
336                                                        value=samlAttrVal.value)
337                           
338                            xacmlAttribute.attributeValues.append(xacmlAttrVal)
339       
340        # Update the XACML request context subject with the new attributes
341        xacmlCtxSubject.attributes.append(xacmlAttribute)
342       
343        # Return the attributes to the caller to comply with the interface
344        return xacmlAttribute.attributeValues
345       
Note: See TracBrowser for help on using the repository browser.