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

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

Incomplete - task 2: XACML-Security Integration

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