source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/zsi.py @ 5667

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/zsi.py@5667
Revision 5667, 16.3 KB checked in by pjkersha, 11 years ago (diff)

Refactoring SOAPBindingMiddleware to accept a ZSI Service binding input from an upstream middleware component.

Line 
1"""WSGI SOAP Middleware utilising the ZSI Python SOAP Package
2
3NERC DataGrid Project
4
5"""
6__author__ = "P J Kershaw"
7__date__ = "19/08/09"
8__copyright__ = "(C) 2009 Science and Technology Facilities Council"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__license__ = "BSD - see LICENSE file in top-level directory"
11__revision__ = "$Id$"
12import logging
13log = logging.getLogger(__name__)
14
15import sys
16
17from ZSI.parse import ParsedSoap
18from ZSI.writer import SoapWriter
19from ZSI import fault
20from ZSI.ServiceContainer import ServiceSOAPBinding
21
22from ndg.security.server.wsgi.soap import SOAPMiddleware, SOAPMiddlewareError,\
23    SOAPMiddlewareConfigError, SOAPMiddlewareReadError
24from ndg.security.common.wssecurity.utils import DomletteReader, \
25    DomletteElementProxy
26from ndg.security.common.utils.classfactory import instantiateClass, \
27    importClass
28     
29class ZSIMiddlewareError(SOAPMiddlewareError):
30    """Base class for ZSI Middleware type exceptions"""
31   
32class ZSIMiddlewareReadError(SOAPMiddlewareReadError):
33    """ZSI Middleware read error"""
34
35class ZSIMiddlewareConfigError(SOAPMiddlewareConfigError):
36    """ZSI middleware configuration error"""
37     
38class ZSIMiddleware(SOAPMiddleware):
39    '''Middleware configurable to a given ZSI SOAP binding''' 
40   
41    soapWriterKey = 'ZSI.writer.SoapWriter'
42    parsedSOAPKey = 'ZSI.parse.ParsedSoap'
43    soapFaultSetKey = 'ndg.security.server.wsgi.soap.soapFault'
44   
45    def __init__(self, app, app_conf, **kw):
46        log.debug("ZSIMiddleware.__init__ ...")
47        self.app = app
48        self.app_conf = app_conf
49        self.app_conf.update(kw)
50       
51        if 'charset' in self.app_conf:
52            self.app_conf['charset'] = '; charset=' + self.app_conf['charset']
53        else:
54            self.app_conf['charset'] = '; charset=utf-8'
55
56        if 'path' in self.app_conf:
57            if self.app_conf['path'] != '/':
58                self.app_conf['path'] = self.app_conf['path'].rstrip('/')
59        else:
60            self.app_conf['path'] = '/'
61
62        # This flag if set to True causes this handler to call the
63        # start_response method and output the SOAP response
64        self.writeResponseSet = self.app_conf.get('writeResponse',
65                                                  'false').lower() == 'true'
66
67        # Check for a list of other filters to be referenced by this one
68        if 'referencedFilters' in self.app_conf:
69            # __call__  may reference any filters in environ keyed by these
70            # keywords
71            self.referencedFilterKeys = self.app_conf.pop(
72                                                'referencedFilters').split()
73                               
74            # Remove equivalent keyword if present
75            kw.pop('referencedFilters', None)
76           
77        # The endpoint that this services will be referenced from externally.
78        # e.g. the Session Manager client running locally can check the
79        # input URI and compare with this value to see if the request is
80        # actually to the local Session Manager instance
81        if 'publishedURI' in self.app_conf:
82            self.publishedURI = self.app_conf.pop('publishedURI')
83           
84        if 'readerclass' in self.app_conf:
85            self.readerclass = importClass(self.app_conf.pop('readerclass'))
86        else:
87            self.readerclass = DomletteReader
88       
89        if 'writerclass' in self.app_conf:
90            self.writerclass = importClass(self.app_conf.pop('writerclass'))
91        else:
92            self.writerclass = DomletteElementProxy
93           
94
95    def __call__(self, environ, start_response):
96        log.debug("ZSIMiddleware.__call__")
97                       
98        # Derived class must implement SOAP Response via overloaded version of
99        # this method.  ParsedSoap object is available as a key in environ via
100        # the parseRequest method
101       
102        return self.writeResponse(environ, start_response)
103
104   
105    def _initCall(self, environ, start_response):
106        '''Sub-divided out from __call__ to enable derived classes to easily
107        include this functionality:
108         - Set a reference to this WSGI filter in environ if filterID was
109        set in the config and
110         - check the request to see if this filter should handle it
111        '''
112       
113        # Add any filter references for this WSGI component regardless of the
114        # current request ID.  This ensures that other WSGI components called
115        # may reference it if they need to.
116        self.addFilter2Environ(environ)
117       
118        # Apply filter for calls
119        if not self.__class__.isSOAPMessage(environ):
120            log.debug("ZSIMiddleware.__call__: skipping non-SOAP call")
121            return self.app(environ, start_response)
122       
123        elif not self.pathMatch(environ):
124            log.debug("ZSIMiddleware.__call__: path doesn't match SOAP "
125                      "service endpoint")
126            return self.app(environ, start_response)
127       
128        elif self.__class__.isSOAPFaultSet(environ):
129            # This MUST be checked in a overloaded version especially in
130            # consideration of security: e.g. an upstream signature
131            # verification may have found an error in a signature
132            log.debug("ZSIMiddleware.__call__: SOAP fault set by previous "
133                      "SOAP middleware filter")
134            return self.app(environ, start_response)
135
136        # Parse input into a ZSI ParsedSoap object set as a key in environ
137        try:
138            self.parseRequest(environ)
139        except Exception, e:
140            sw = self.exception2SOAPFault(environ, e)
141            self.setSOAPWriter(environ, sw)
142            return self.writeResponse(environ, start_response)
143       
144        # Return None to __call__ to indicate that it can proceed with
145        # processing the input
146        return None
147
148    def exception2SOAPFault(self, environ, exception):
149        '''Convert an exception into a SOAP fault message'''
150        soapFault = fault.FaultFromException(exception, 
151                                             None,
152                                             tb=sys.exc_info()[2])
153        sw = SoapWriter(outputclass=self.writerclass)
154        soapFault.serialize(sw)
155        environ[ZSIMiddleware.soapFaultSetKey] = 'True'
156        return sw
157   
158    pathMatch = lambda self,environ:environ['PATH_INFO']==self.app_conf['path']
159   
160    def parseRequest(self, environ):
161        '''Parse SOAP message from environ['wsgi.input']
162       
163        Reading from environ['wsgi.input'] may be a destructive process so the
164        content is saved in a ZSI.parse.ParsedSoap object for use by SOAP
165        handlers which follow in the chain
166       
167        environ['ZSI.parse.ParsedSoap'] may be set to a ParsedSoap object
168        parsed by a SOAP handler ahead of the current one in the chain.  In
169        this case, don't re-parse.  If NOT parsed, parse and set
170        'ZSI.parse.ParsedSoap' environ key'''
171       
172        # Check for ParsedSoap object set in environment, if not present,
173        # make one
174        ps = environ.get(ZSIMiddleware.parsedSOAPKey)
175        if ps is None:
176            # TODO: allow for chunked data
177            contentLength = int(environ['CONTENT_LENGTH'])
178            soapIn = environ['wsgi.input'].read(contentLength)
179            if len(soapIn) < contentLength:
180                raise ZSIMiddlewareReadError("Expecting %d content length; "
181                                              "received %d instead." % 
182                                              (contentLength, len(soapIn)))
183           
184            log.debug("SOAP Request for handler %r" % ZSIMiddleware)
185            log.debug("_" * 80)
186            log.debug(soapIn)
187            log.debug("_" * 80)
188           
189            ps = ParsedSoap(soapIn, readerclass=self.readerclass)
190            environ[ZSIMiddleware.parsedSOAPKey] = ps
191           
192        return environ[ZSIMiddleware.parsedSOAPKey]
193
194
195    def writeResponse(self, environ, start_response, errorCode=None):
196        '''This method serializes the SOAP output and sets the response header.
197        It's the final step and should be called in the last SOAP handler in
198        a chain of handlers or else specify it in the ini file as the last
199        SOAP handler'''
200       
201        # This flag must be set to True to write out the final response from
202        # this handler
203        if self.writeResponseSet == False:
204            return self.app(environ, start_response)
205       
206        sw = self.getSOAPWriter(environ)
207        soapOut = str(sw)
208        charset = self.app_conf['charset']
209       
210        if errorCode is None:
211            if self.__class__.isSOAPFaultSet(environ):
212                errorCode = "500 Internal Server Error"
213            else:
214                errorCode = "200 OK"
215               
216        log.debug("SOAP Response for handler %r" % self.__class__)
217        log.debug("_" * 80)
218        log.debug(soapOut)
219        log.debug("_" * 80)
220        start_response(errorCode,
221                       [('content-type', 'text/xml' + charset),
222                        ('content-length', str(len(soapOut)))])
223        return soapOut
224
225    @classmethod
226    def getSOAPWriter(cls, environ):
227        '''Access SoapWriter object set in environment by this classes' call
228        method'''
229       
230        sw = environ.get(ZSIMiddleware.soapWriterKey)
231        if sw is None:
232            raise KeyError("Expecting '%s' key in environ: missing call to "
233                           "ZSIMiddleware?" % ZSIMiddleware.soapWriterKey)
234        return sw
235   
236    @classmethod
237    def setSOAPWriter(cls, environ, sw):
238        '''Set SoapWriter object in environment'''   
239        environ[ZSIMiddleware.soapWriterKey] = sw
240
241    def addFilter2Environ(self, environ):
242        '''Add a key to the current application in the environment so that
243        other middleware can reference it.  This is dependent on filterID set
244        in app_conf'''
245        filterID = self.app_conf.get('filterID')
246        if filterID is not None:           
247            if filterID in environ:
248                raise ZSIMiddlewareConfigError("An filterID key '%s' is "
249                                                "already set in environ" % 
250                                                filterID)
251            environ[filterID] = self
252
253
254class SOAPBindingMiddleware(ZSIMiddleware): 
255    '''Apply a ZSI ServiceSOAPBinding type SOAP service'''
256         
257    def __init__(self, *arg, **kw):
258       
259        super(SOAPBindingMiddleware, self).__init__(*arg, **kw)
260        self.__serviceSOAPBinding = None
261        self.__enableWSDLQuery = False
262
263    def _getServiceSOAPBinding(self):
264        return self.__serviceSOAPBinding
265
266    def _setServiceSOAPBinding(self, value):
267        """Instance must be ZSI ServiceSOAPBinding derived type"""
268        if not isinstance(value, ServiceSOAPBinding):
269            raise TypeError('Expecting bool type for "enableWSDLQuery"; got '
270                            '%r' % type(value))
271        self.__serviceSOAPBinding = value
272
273    serviceSOAPBinding = property(fget=_getServiceSOAPBinding, 
274                                  fset=_setServiceSOAPBinding, 
275                                  doc="ServiceSOAPBinding's Docstring")
276
277    def _getEnableWSDLQuery(self):
278        return self.__enableWSDLQuery
279
280    def _setEnableWSDLQuery(self, value):
281        if not isinstance(value, bool):
282            raise TypeError('Expecting bool type for "enableWSDLQuery"; got '
283                            '%r' % type(value))
284        self.__enableWSDLQuery = value
285       
286    enableWSDLQuery = property(fget=_getEnableWSDLQuery, 
287                               fset=_setEnableWSDLQuery, 
288                               doc="EnableWSDLQuery's Docstring")
289
290       
291#        # Check for Service binding in config
292#        if 'ServiceSOAPBindingClass' in self.app_conf:
293#            modName, className = \
294#                        self.app_conf['ServiceSOAPBindingClass'].rsplit('.', 1)
295#
296#            self.serviceSOAPBinding = instantiateClass(modName,
297#                                   className,
298#                                   objectType=ServiceSOAPBinding,
299#                                   classProperties=self.serviceSOAPBindingKw)           
300#        else:
301#            self.serviceSOAPBinding = ServiceSOAPBinding()
302#       
303#        # Flag to enable display of WSDL via wsdl query arg in a GET request
304#        self.enableWSDLQuery = self.app_conf.get('enableWSDLQuery', False) and\
305#                                hasattr(self.serviceSOAPBinding, '_wsdl')
306
307    @classmethod
308    def filter_app_factory(cls, app, global_conf, **app_conf):
309       
310        app = cls(app)
311       
312        # Check for Service binding in config
313        if 'ServiceSOAPBindingClass' in app.app_conf:
314            modName, className = \
315                            app_conf['ServiceSOAPBindingClass'].rsplit('.', 1)
316
317            self.serviceSOAPBinding = instantiateClass(modName,
318                                   className,
319                                   objectType=ServiceSOAPBinding,
320                                   classProperties=self.serviceSOAPBindingKw)           
321        else: 
322            self.serviceSOAPBinding = ServiceSOAPBinding()
323       
324        # Flag to enable display of WSDL via wsdl query arg in a GET request
325        app.enableWSDLQuery = self.app_conf.get('enableWSDLQuery', False) and\
326                                hasattr(self.serviceSOAPBinding, '_wsdl')
327                               
328        # Check for WSGI Service SOAP Binding
329        if callable(self.serviceSOAPBinding):
330            app = self.serviceSOAPBinding(app)
331           
332        return app
333   
334    def _getServiceSOAPBindingKw(self):
335        '''Extract keywords to specific to SOAP Service Binding set in app_conf
336        '''
337        if 'ServiceSOAPBindingPropPrefix' not in self.app_conf:
338            return {}
339       
340        prefix = self.app_conf['ServiceSOAPBindingPropPrefix'] + '.'
341        serviceSOAPBindingKw = dict([(k.replace(prefix, ''), v) \
342                                     for k, v in self.app_conf.items() \
343                                     if k.startswith(prefix)])
344        return serviceSOAPBindingKw
345   
346    serviceSOAPBindingKw = property(fget=_getServiceSOAPBindingKw)
347   
348    def __call__(self, environ, start_response):
349        log.debug("SOAPBindingMiddleware.__call__ ...")
350               
351        if self.pathMatch(environ) and self.enableWSDLQuery and \
352           environ.get('REQUEST_METHOD', '') == 'GET' and \
353           environ.get('QUERY_STRING', '') == 'wsdl':
354            wsdl = self.serviceSOAPBinding._wsdl
355            start_response("200 OK", [('Content-type', 'text/xml'),
356                                      ('Content-length', str(len(wsdl)))])
357            return wsdl
358               
359        # Apply filter for calls
360        response = self._initCall(environ, start_response)
361        if response is not None:
362            return response
363               
364        try:
365            # Other filters in the middleware chain may be passed by setting
366            # a reference to them in the config.  This is useful if the SOAP
367            # binding code needs to access results from upstream middleware
368            # e.g. check output from signature verification filter
369            if hasattr(self, 'referencedFilterKeys'):
370                try:
371                    self.serviceSOAPBinding.referencedWSGIFilters = \
372                                    dict([(i, environ[i]) 
373                                          for i in self.referencedFilterKeys])
374                except KeyError:
375                    raise ZSIMiddlewareConfigError('No filter ID "%s" found '
376                                                    'in environ' % i)   
377            ps = self.parseRequest(environ)
378               
379            # Map SOAP Action to method in binding class
380            soapMethodName = 'soap_%s' % environ['HTTP_SOAPACTION'].strip('"')
381           
382            method = getattr(self.serviceSOAPBinding, soapMethodName)           
383            resp = method(ps)
384        except Exception, e:
385            sw = self.exception2SOAPFault(environ, e)
386        else: 
387            # Serialize output using SOAP writer class
388            sw = SoapWriter(outputclass=self.writerclass)
389            sw.serialize(resp)
390       
391        # Make SoapWriter object available to any SOAP filters that follow
392        self.setSOAPWriter(environ, sw)
393       
394        soapOut = str(sw)
395        charset = self.app_conf['charset']
396
397        return self.writeResponse(environ, start_response)
Note: See TracBrowser for help on using the repository browser.