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

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

Incomplete - task 2: XACML-Security Integration

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