source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/saml/__init__.py @ 6686

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/saml/__init__.py@6686
Revision 6686, 14.5 KB checked in by pjkersha, 10 years ago (diff)

Refactoring Attribute Authority to remove NDG Attribute Certificate and role mapping code.

Line 
1"""WSGI SAML package for SAML 2.0 Attribute and Authorisation Decision Query/
2Request Profile interfaces
3
4NERC DataGrid Project
5"""
6__author__ = "P J Kershaw"
7__date__ = "15/02/10"
8__copyright__ = "(C) 2010 Science and Technology Facilities Council"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = "$Id: $"
11__license__ = "BSD - see LICENSE file in top-level directory"
12import logging
13log = logging.getLogger(__name__)
14import traceback
15from cStringIO import StringIO
16from uuid import uuid4
17from datetime import datetime
18from xml.etree import ElementTree
19
20from ndg.saml.saml2.core import Response, AttributeQuery, Status, StatusCode
21from ndg.saml.xml import UnknownAttrProfile
22from ndg.saml.xml.etree import AttributeQueryElementTree, ResponseElementTree
23
24from ndg.security.common.saml_utils.esg import XSGroupRoleAttributeValue
25from ndg.security.common.saml_utils.esg.xml.etree import (
26                                        XSGroupRoleAttributeValueElementTree)
27from ndg.security.common.soap.etree import SOAPEnvelope
28from ndg.security.common.utils.factory import importModuleObject
29from ndg.security.server.wsgi import NDGSecurityPathFilter
30from ndg.security.server.wsgi.soap import SOAPMiddleware
31
32
33class SOAPQueryInterfaceMiddlewareError(Exception):
34    """Base class for WSGI SAML 2.0 SOAP Query Interface Errors"""
35
36
37class SOAPQueryInterfaceMiddlewareConfigError(Exception):
38    """WSGI SAML 2.0 SOAP Query Interface Configuration problem"""
39
40 
41class SOAPQueryInterfaceMiddleware(SOAPMiddleware, NDGSecurityPathFilter):
42    """Implementation of SAML 2.0 SOAP Binding for Query/Request Binding
43   
44    @type PATH_OPTNAME: basestring
45    @cvar PATH_OPTNAME: name of app_conf option for specifying a path or paths
46    that this middleware will intercept and process
47    @type QUERY_INTERFACE_KEYNAME_OPTNAME: basestring
48    @cvar QUERY_INTERFACE_KEYNAME_OPTNAME: app_conf option name for key name
49    used to reference the SAML query interface in environ
50    @type DEFAULT_QUERY_INTERFACE_KEYNAME: basestring
51    @param DEFAULT_QUERY_INTERFACE_KEYNAME: default key name for referencing
52    SAML query interface in environ
53    """
54    log = logging.getLogger('SOAPQueryInterfaceMiddleware')
55    PATH_OPTNAME = "pathMatchList"
56    QUERY_INTERFACE_KEYNAME_OPTNAME = "queryInterfaceKeyName"
57    DEFAULT_QUERY_INTERFACE_KEYNAME = ("ndg.security.server.wsgi.saml."
58                            "SOAPQueryInterfaceMiddleware.queryInterface")
59   
60    REQUEST_ENVELOPE_CLASS_OPTNAME = 'requestEnvelopeClass'
61    RESPONSE_ENVELOPE_CLASS_OPTNAME = 'responseEnvelopeClass'
62    SERIALISE_OPTNAME = 'serialise'
63    DESERIALISE_OPTNAME = 'deserialise' 
64     
65    CONFIG_FILE_OPTNAMES = (
66        PATH_OPTNAME,
67        QUERY_INTERFACE_KEYNAME_OPTNAME,
68        DEFAULT_QUERY_INTERFACE_KEYNAME,
69        REQUEST_ENVELOPE_CLASS_OPTNAME,
70        RESPONSE_ENVELOPE_CLASS_OPTNAME,
71        SERIALISE_OPTNAME,
72        DESERIALISE_OPTNAME
73    )
74   
75    def __init__(self, app):
76        '''@type app: callable following WSGI interface
77        @param app: next middleware application in the chain
78        '''     
79        NDGSecurityPathFilter.__init__(self, app, None)
80       
81        self._app = app
82       
83        # Set defaults
84        cls = SOAPQueryInterfaceMiddleware
85        self.__queryInterfaceKeyName = cls.DEFAULT_QUERY_INTERFACE_KEYNAME
86        self.pathMatchList = ['/']
87        self.__requestEnvelopeClass = None
88        self.__responseEnvelopeClass = None
89        self.__serialise = None
90        self.__deserialise = None
91        self.__issuer = None
92        self.__clockSkewTolerance = 0.
93        self.__verifyTimeConditions = True
94                 
95    def initialise(self, global_conf, prefix='', **app_conf):
96        '''
97        @type global_conf: dict       
98        @param global_conf: PasteDeploy global configuration dictionary
99        @type prefix: basestring
100        @param prefix: prefix for configuration items
101        @type app_conf: dict       
102        @param app_conf: PasteDeploy application specific configuration
103        dictionary
104        '''
105        cls = SOAPQueryInterfaceMiddleware
106       
107        # Override where set in config
108        for name in SOAPQueryInterfaceMiddleware.CONFIG_FILE_OPTNAMES:
109            val = app_conf.get(prefix + name)
110            if val is not None:
111                setattr(self, name, val)
112
113        if self.serialise is None:
114            raise AttributeError('No "serialise" method set to serialise the '
115                                 'SAML response from this middleware.')
116
117        if self.deserialise is None:
118            raise AttributeError('No "deserialise" method set to parse the '
119                                 'SAML request to this middleware.')
120           
121    def _getSerialise(self):
122        return self.__serialise
123
124    def _setSerialise(self, value):
125        if isinstance(value, basestring):
126            self.__serialise = importModuleObject(value)
127           
128        elif callable(value):
129            self.__serialise = value
130        else:
131            raise TypeError('Expecting callable for "serialise"; got %r' % 
132                            value)
133
134    serialise = property(_getSerialise, _setSerialise, 
135                         doc="callable to serialise request into XML type")
136
137    def _getDeserialise(self):
138        return self.__deserialise
139
140    def _setDeserialise(self, value):
141        if isinstance(value, basestring):
142            self.__deserialise = importModuleObject(value)
143           
144        elif callable(value):
145            self.__deserialise = value
146        else:
147            raise TypeError('Expecting callable for "deserialise"; got %r' % 
148                            value)
149       
150    deserialise = property(_getDeserialise, 
151                           _setDeserialise, 
152                           doc="callable to de-serialise response from XML "
153                               "type")       
154
155    def _getIssuer(self):
156        return self.__issuer
157
158    def _setIssuer(self, value):
159        if not isinstance(value, basestring):
160            raise TypeError('Expecting string type for "issuer"; got %r' %
161                            type(value))
162           
163        self.__issuer = value
164       
165    issuer = property(fget=_getIssuer, 
166                      fset=_setIssuer, 
167                      doc="Name of issuing authority")
168
169    def _getVerifyTimeConditions(self):
170        return self.__verifyTimeConditions
171
172    def _setVerifyTimeConditions(self, value):
173        if isinstance(value, bool):
174            self.__verifyTimeConditions = value
175           
176        if isinstance(value, basestring):
177            self.__verifyTimeConditions = str2Bool(value)
178        else:
179            raise TypeError('Expecting bool or string type for '
180                            '"verifyTimeConditions"; got %r instead' % 
181                            type(value))
182
183    verifyTimeConditions = property(_getVerifyTimeConditions, 
184                                    _setVerifyTimeConditions, 
185                                    doc='Set to True to verify any time '
186                                        'Conditions set in the returned '
187                                        'response assertions')
188   
189    def _getClockSkewTolerance(self):
190        return self.__clockSkewTolerance
191
192    def _setClockSkewTolerance(self, value):
193        if isinstance(value, (float, int, long)):
194            self.__clockSkewTolerance = timedelta(seconds=value)
195           
196        elif isinstance(value, basestring):
197            self.__clockSkewTolerance = timedelta(seconds=float(value))
198        else:
199            raise TypeError('Expecting float, int, long or string type for '
200                            '"clockSkew"; got %r' % type(value)) 
201               
202    clockSkewTolerance = property(fget=_getClockSkewTolerance, 
203                                  fset=_setClockSkewTolerance, 
204                                  doc="Set a tolerance of +/- n seconds to "
205                                      "allow for clock skew when checking "
206                                      "timestamps of client queries")
207
208    @classmethod
209    def filter_app_factory(cls, app, global_conf, **app_conf):
210        """Set-up using a Paste app factory pattern.  Set this method to avoid
211        possible conflicts from multiple inheritance
212       
213        @type app: callable following WSGI interface
214        @param app: next middleware application in the chain     
215        @type global_conf: dict       
216        @param global_conf: PasteDeploy global configuration dictionary
217        @type prefix: basestring
218        @param prefix: prefix for configuration items
219        @type app_conf: dict       
220        @param app_conf: PasteDeploy application specific configuration
221        dictionary
222        """
223        app = cls(app)
224        app.initialise(global_conf, **app_conf)
225       
226        return app
227   
228    def _getQueryInterfaceKeyName(self):
229        return self.__queryInterfaceKeyName
230
231    def _setQueryInterfaceKeyName(self, value):
232        if not isinstance(value, basestring):
233            raise TypeError('Expecting string type for "queryInterfaceKeyName"'
234                            ' got %r' % value)
235           
236        self.__queryInterfaceKeyName = value
237
238    queryInterfaceKeyName = property(fget=_getQueryInterfaceKeyName, 
239                                     fset=_setQueryInterfaceKeyName, 
240                                     doc="environ key name for Attribute Query "
241                                         "interface")
242   
243    @NDGSecurityPathFilter.initCall
244    def __call__(self, environ, start_response):
245        """Check for and parse a SOAP SAML Attribute Query and return a
246        SAML Response
247       
248        @type environ: dict
249        @param environ: WSGI environment variables dictionary
250        @type start_response: function
251        @param start_response: standard WSGI start response function
252        """
253   
254        # Ignore non-matching path
255        if not self.pathMatch:
256            return self._app(environ, start_response)
257         
258        # Ignore non-POST requests
259        if environ.get('REQUEST_METHOD') != 'POST':
260            return self._app(environ, start_response)
261       
262        soapRequestStream = environ.get('wsgi.input')
263        if soapRequestStream is None:
264            raise SOAPQueryInterfaceMiddlewareError('No "wsgi.input" in '
265                                                    'environ')
266       
267        # TODO: allow for chunked data
268        contentLength = environ.get('CONTENT_LENGTH')
269        if contentLength is None:
270            raise SOAPQueryInterfaceMiddlewareError('No "CONTENT_LENGTH" in '
271                                                    'environ')
272
273        contentLength = int(contentLength)
274        soapRequestTxt = soapRequestStream.read(contentLength)
275       
276        # Parse into a SOAP envelope object
277        soapRequest = SOAPEnvelope()
278        soapRequest.parse(StringIO(soapRequestTxt))
279       
280        log.debug("SOAPQueryInterfaceMiddleware.__call__: received SAML "
281                  "SOAP AttributeQuery ...")
282       
283        queryElem = soapRequest.body.elem[0]
284       
285        # Create a response with basic attributes if provided in the
286        # initialisation config
287        samlResponse = self._initResponse()
288       
289        try:
290            samlQuery = self.deserialise(queryElem)
291           
292        except UnknownAttrProfile:
293            log.exception("%r raised parsing incoming query: " % 
294                          (type(e), traceback.format_exc()))
295            samlResponse.statusCode.value = StatusCode.UNKNOWN_ATTR_PROFILE_URI
296        else:   
297            # Check for Query Interface in environ
298            queryInterface = environ.get(self.queryInterfaceKeyName)
299            if queryInterface is None:
300                raise SOAPQueryInterfaceMiddlewareConfigError(
301                                'No query interface "%s" key found in environ' %
302                                self.queryInterfaceKeyName)
303           
304            # Basic validation
305            self._validateQuery(samlQuery)
306           
307            samlResponse.inResponseTo = samlQuery.id
308           
309            # Call query interface       
310            queryInterface(samlQuery, samlResponse)
311       
312        # Convert to ElementTree representation to enable attachment to SOAP
313        # response body
314        samlResponseElem = self.serialise(samlResponse)
315       
316        # Create SOAP response and attach the SAML Response payload
317        soapResponse = SOAPEnvelope()
318        soapResponse.create()
319        soapResponse.body.elem.append(samlResponseElem)
320       
321        response = soapResponse.serialize()
322       
323        log.debug("SOAPQueryInterfaceMiddleware.__call__: sending response "
324                  "...\n\n%s",
325                  response)
326        start_response("200 OK",
327                       [('Content-length', str(len(response))),
328                        ('Content-type', 'text/xml')])
329        return [response]
330   
331    def _validateQuery(self, query):
332        if not self.verifyTimeConditions:
333            log.debug("Skipping verification of SAML Response time conditions")
334           
335        utcNow = datetime.utcnow() 
336        nowPlusSkew = utcNow + self.clockSkewTolerance
337       
338        if response.issueInstant > nowPlusSkew:
339            msg = ('SAML Attribute Query issueInstant [%s] is after '
340                   'the clock time [%s] (skewed +%s)' % 
341                   (query.issueInstant, 
342                    SAMLDateTime.toString(nowPlusSkew),
343                    self.clockSkewTolerance))
344             
345            samlRespError = QueryIssueInstantInvalid(msg)
346            samlRespError.response = response
347            raise samlRespError
348
349    def _initResponse(self):
350        """Create a SAML Response object with basic settings if any have been
351        provided at initialisation of this class - see initialise
352       
353        @return: SAML response object
354        @rtype: ndg.saml.saml2.core.Response
355        """
356        samlResponse = Response()
357        utcNow = datetime.utcnow()
358       
359        samlResponse.issueInstant = utcNow
360        samlResponse.id = str(uuid4())
361        samlResponse.issuer = Issuer()
362       
363        # Nb. SAML 2.0 spec says issuer format must be omitted
364        if self.issuer is not None:
365            samlResponse.issuer.value = self.issuer
366       
367        # Initialise to success status but reset on error
368        samlResponse.status = Status()
369        samlResponse.status.statusCode = StatusCode()
370        samlResponse.status.statusMessage = StatusMessage()
371        samlResponse.status.statusCode.value = StatusCode.SUCCESS_URI
372       
373        samlResponse.status.statusMessage = StatusMessage()
374
375        return samlResponse
376
Note: See TracBrowser for help on using the repository browser.