source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/soap.py @ 4185

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

Refined SOAP and WS-Security WSGI middleware. TODO: pass all Attribute Authority unit tests and integrate Session Manager.

Line 
1"""NDG Security SOAP Service Middleware
2
3NERC Data Grid Project
4
5This software may be distributed under the terms of the Q Public License,
6version 1.0 or later.
7"""
8__author__ = "P J Kershaw"
9__date__ = "27/05/08"
10__copyright__ = "(C) 2008 STFC & NERC"
11__contact__ = "P.J.Kershaw@rl.ac.uk"
12__revision__ = "$Id$"
13import logging
14log = logging.getLogger(__name__)
15
16import paste.request
17from ZSI import EvaluateException, ParseException
18from ZSI.parse import ParsedSoap
19from ZSI.writer import SoapWriter
20from ZSI import fault
21
22from ZSI.ServiceContainer import ServiceSOAPBinding
23       
24class SOAPMiddlewareError(Exception):
25    """Base error handling exception for this module"""
26   
27class SOAPMiddlewareReadError(SOAPMiddlewareError):
28    """SOAP read error"""
29   
30class SOAPMiddleware(object):
31    '''Middleware configurable to a given ZSI SOAP binding''' 
32   
33    soapWriterKey = 'ZSI.writer.SoapWriter'
34    parsedSOAPKey = 'ZSI.parse.ParsedSoap'
35    soapFaultSetKey = 'ndg.security.server.wsgi.soap.soapFault'
36   
37    def __init__(self, app, app_conf, **kw):
38        log.debug("SOAPMiddleware.__init__ ...")
39        self.app = app
40        self.app_conf = app_conf
41        self.app_conf.update(kw)
42       
43        if 'charset' in self.app_conf:
44            self.app_conf['charset'] = '; charset=' + self.app_conf['charset']
45        else:
46            self.app_conf['charset'] = '; charset=utf-8'
47
48        if 'path' in self.app_conf:
49            if self.app_conf['path'] != '/':
50                self.app_conf['path'] = self.app_conf['path'].rstrip('/')
51        else:
52            self.app_conf['path'] = '/'
53
54        # This flag if set to True causes this handler to call the
55        # start_response method and output the SOAP response
56        self.writeResponseSet = bool(self.app_conf.get('writeResponse', False))
57
58
59    def __call__(self, environ, start_response):
60        log.debug("SOAPMiddleware.__call__")
61                       
62        # Apply filter for calls
63        if not self.isSOAPMessage(environ):
64            log.debug("SOAPMiddleware.__call__: skipping non-SOAP call")
65            return self.app(environ, start_response)
66       
67        elif not self.pathMatch(environ):
68            log.debug("SOAPMiddleware.__call__: path doesn't match SOAP "
69                      "service endpoint")
70            return self.app(environ, start_response)
71       
72        elif self.isSOAPFaultSet(environ):
73            # This MUST be checked in a overloaded version especially in
74            # consideration of security: e.g. an upstream signature
75            # verification may have found an error in a signature
76            log.debug("SOAPMiddleware.__call__: SOAP fault set by previous "
77                      "SOAP middleware filter")
78            return self.app(environ, start_response)
79
80        self.parseRequest(environ)
81
82        # Derived class must implement SOAP Response via overloaded version of
83        # this method.  ParsedSoap object is available as a key in environ via
84        # the parseRequest method
85       
86        return self.writeResponse(environ, start_response)
87
88    @classmethod
89    def exception2SOAPFault(cls, environ, exception):
90        '''Convert an exception into a SOAP fault message'''
91        soapFault = fault.FaultFromException(exception, None)
92        sw = SoapWriter()
93        soapFault.serialize(sw)
94        environ[cls.soapFaultSetKey] = 'True'
95        return sw
96   
97    pathMatch = lambda self,environ:environ['PATH_INFO']==self.app_conf['path']
98       
99    @staticmethod
100    def isSOAPMessage(environ):
101        '''Generic method to filter out non-soap messages
102       
103        TODO: is HTTP_SOAPACTION only set for WSDL doc-literal wrapped style
104        generated content? - If so this test should be moved'''
105        return environ.get('REQUEST_METHOD', '') == 'POST' and \
106               environ.get('HTTP_SOAPACTION') is not None
107
108    @classmethod
109    def isSOAPFaultSet(cls, environ):
110        '''Check environment for SOAP fault flag set.  This variable is set
111        from exception2SOAPFault'''
112        return bool(environ.get(cls.soapFaultSetKey, False)) == True
113   
114    @classmethod
115    def parseRequest(cls, environ):
116        '''Parse SOAP message from environ['wsgi.input']
117       
118        Reading from environ['wsgi.input'] may be a destructive process so the
119        content is saved in a ZSI.parse.ParsedSoap object for use by SOAP
120        handlers which follow in the chain
121       
122        environ['ZSI.parse.ParsedSoap'] may be set to a ParsedSoap object
123        parsed by a SOAP handler ahead of the current one in the chain.  In
124        this case, don't re-parse.  If NOT parsed, parse and set
125        'ZSI.parse.ParsedSoap' environ key'''
126       
127        # Check for ParsedSoap object set in environment, if not present,
128        # make one
129        ps = environ.get(cls.parsedSOAPKey)
130        if ps is None:
131            # TODO: allow for chunked data
132            contentLength = int(environ['CONTENT_LENGTH'])
133            soapIn = environ['wsgi.input'].read(contentLength)
134            if len(soapIn) < contentLength:
135                raise SOAPMiddlewareReadError("Expecting %s content length; "
136                                              "received %d instead." % \
137                                              (environ['CONTENT_LENGTH'],
138                                               len(soapIn)))
139           
140            log.debug("SOAP Request for handler %r" % cls)
141            log.debug("_"*80)
142            log.debug(soapIn)
143            log.debug("_"*80)
144           
145            ps = ParsedSoap(soapIn)
146            environ[cls.parsedSOAPKey] = ps
147           
148        return environ[cls.parsedSOAPKey]
149
150
151    def writeResponse(self, environ, start_response, errorCode=None):
152        '''This method serializes the SOAP output and sets the response header.
153        It's the final step and should be called in the last SOAP handler in
154        a chain of handlers or else specify it in the ini file as the last
155        SOAP handler'''
156       
157        # This flag must be set to True to write out the final response from
158        # this handler
159        if self.writeResponseSet == False:
160            return self.app(environ, start_response)
161       
162        sw = self.getSOAPWriter(environ)
163        soapOut = str(sw)
164        charset = self.app_conf['charset']
165       
166        if errorCode is None:
167            if self.isSOAPFaultSet(environ):
168                errorCode = "500 Internal Server Error"
169            else:
170                errorCode = "200 OK"
171               
172        start_response(errorCode,
173                       [('content-type', 'text/xml'+charset),
174                        ('content-length', str(len(soapOut)))])
175        return soapOut
176
177    @classmethod
178    def getSOAPWriter(cls, environ):
179        '''Access SoapWriter object set in environment by this classes' call
180        method'''
181       
182        sw = environ.get(SOAPMiddleware.soapWriterKey)
183        if sw is None:
184            raise KeyError("Expecting '%s' key in environ: missing call to "
185                           "SOAPMiddleware?" % SOAPMiddleware.soapWriterKey)
186        return sw
187   
188    @classmethod
189    def setSOAPWriter(cls, environ, sw):
190        '''Set SoapWriter object in environment'''   
191        environ[SOAPMiddleware.soapWriterKey] = sw
192
193
194class SOAPBindingMiddleware(SOAPMiddleware): 
195    '''Apply a ZSI ServiceSOAPBinding type SOAP service'''
196         
197    def __init__(self, *arg, **kw):
198        super(SOAPBindingMiddleware, self).__init__(*arg, **kw)
199       
200        # Check for Service binding in config
201        if 'ServiceSOAPBindingClass' in self.app_conf:
202            modName, dot, className = \
203                    self.app_conf['ServiceSOAPBindingClass'].rpartition('.')
204           
205            module = __import__(modName, globals(), locals(), [className])
206            serviceSOAPBindingClass = getattr(module, className)
207           
208            # Check class inherits from ServiceSOAPBinding
209            if not issubclass(serviceSOAPBindingClass, ServiceSOAPBinding):
210                raise TypeError("%s class must be derived from "
211                                "ServiceSOAPBinding" % \
212                                self.app_conf['ServiceSOAPBindingClass'])
213        else: 
214            serviceSOAPBindingClass = ServiceSOAPBinding
215                 
216        self.serviceSOAPBinding = serviceSOAPBindingClass()
217       
218        # Flag to enable display of WSDL via wsdl query arg in a GET request
219        self.enableWSDLQuery = self.app_conf.get('enableWSDLQuery', False) and\
220                                hasattr(self.serviceSOAPBinding, '_wsdl')
221
222
223    def __call__(self, environ, start_response):
224        log.debug("SOAPBindingMiddleware.__call__")
225               
226        if environ.get('REQUEST_METHOD') == 'GET' and \
227           environ.get('QUERY_STRING') == 'wsdl':
228            if self.enableWSDLQuery:
229                wsdl = self.serviceSOAPBinding._wsdl
230                start_response("200 OK", [('Content-type', 'text/xml'),
231                                          ('Content-length', str(len(wsdl)))])
232                return wsdl
233               
234               
235        # Apply filter for calls
236        if not self.isSOAPMessage(environ) or \
237           not self.pathMatch(environ) or \
238           self.isSOAPFaultSet(environ):
239            return self.app(environ, start_response)
240
241        try:
242            ps = self.parseRequest(environ)
243           
244            # Map SOAP Action to method in binding class
245            soapMethodName = 'soap_%s' % environ['HTTP_SOAPACTION'].strip('"')
246           
247            method = getattr(self.serviceSOAPBinding, soapMethodName)
248           
249            # TODO: change method to return response only: request, response
250            # tuple is carry over from Twisted based code
251            req, resp = method(ps)
252        except Exception, e:
253            sw = self.exception2SOAPFault(environ, e)
254        else: 
255            # Serialize output using SOAP writer class
256            sw = SoapWriter()
257            sw.serialize(resp)
258       
259        # Make SoapWriter object available to any SOAP filters that follow
260        self.setSOAPWriter(environ, sw)
261       
262        soapOut = str(sw)
263        charset = self.app_conf['charset']
264               
265        log.debug("SOAP Response")
266        log.debug("_"*80)
267        log.debug(soapOut)
268        log.debug("_"*80)
269
270        return self.writeResponse(environ, start_response)
Note: See TracBrowser for help on using the repository browser.