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

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

Working unit tests for WSGI based Attribute Authority.

  • Altered so that all Attribute Config is picked up from the Paste ini file. Separate cfg or xml based config file is still supported.

TODO:

  • Simplify unit test config for client.
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
23from ndg.security.common.utils.ClassFactory import instantiateClass
24     
25class SOAPMiddlewareError(Exception):
26    """Base error handling exception class for the SOAP WSGI middleware module
27    """
28   
29class SOAPMiddlewareReadError(SOAPMiddlewareError):
30    """SOAP Middleware read error"""
31
32class SOAPMiddlewareConfigError(SOAPMiddlewareError):
33    """SOAP Middleware configuration error"""
34
35class SOAPMiddleware(object):
36    '''Middleware configurable to a given ZSI SOAP binding''' 
37   
38    soapWriterKey = 'ZSI.writer.SoapWriter'
39    parsedSOAPKey = 'ZSI.parse.ParsedSoap'
40    soapFaultSetKey = 'ndg.security.server.wsgi.soap.soapFault'
41   
42    def __init__(self, app, app_conf, **kw):
43        log.debug("SOAPMiddleware.__init__ ...")
44        self.app = app
45        self.app_conf = app_conf
46        self.app_conf.update(kw)
47       
48        if 'charset' in self.app_conf:
49            self.app_conf['charset'] = '; charset=' + self.app_conf['charset']
50        else:
51            self.app_conf['charset'] = '; charset=utf-8'
52
53        if 'path' in self.app_conf:
54            if self.app_conf['path'] != '/':
55                self.app_conf['path'] = self.app_conf['path'].rstrip('/')
56        else:
57            self.app_conf['path'] = '/'
58
59        # This flag if set to True causes this handler to call the
60        # start_response method and output the SOAP response
61        self.writeResponseSet = bool(self.app_conf.get('writeResponse', False))
62
63
64    def __call__(self, environ, start_response):
65        log.debug("SOAPMiddleware.__call__")
66                       
67        # Derived class must implement SOAP Response via overloaded version of
68        # this method.  ParsedSoap object is available as a key in environ via
69        # the parseRequest method
70       
71        return self.writeResponse(environ, start_response)
72
73   
74    def _initCall(self, environ, start_response):
75        # Apply filter for calls
76        if not self.isSOAPMessage(environ):
77            log.debug("SOAPMiddleware.__call__: skipping non-SOAP call")
78            return self.app(environ, start_response)
79       
80        elif not self.pathMatch(environ):
81            log.debug("SOAPMiddleware.__call__: path doesn't match SOAP "
82                      "service endpoint")
83            return self.app(environ, start_response)
84       
85        elif self.isSOAPFaultSet(environ):
86            # This MUST be checked in a overloaded version especially in
87            # consideration of security: e.g. an upstream signature
88            # verification may have found an error in a signature
89            log.debug("SOAPMiddleware.__call__: SOAP fault set by previous "
90                      "SOAP middleware filter")
91            return self.app(environ, start_response)
92
93        # Parse input into a ZSI ParsedSoap object set as a key in environ
94        try:
95            self.parseRequest(environ)
96        except Exception, e:
97            sw = self.exception2SOAPFault(environ, e)
98            self.setSOAPWriter(environ, sw)
99            return self.writeResponse(environ, start_response)
100       
101        self.addFilter2Environ(environ)
102       
103        # Return None to __call__ to indicate that it can proceed with
104        # processing the input
105        return None
106
107    @classmethod
108    def exception2SOAPFault(cls, environ, exception):
109        '''Convert an exception into a SOAP fault message'''
110        soapFault = fault.FaultFromException(exception, None)
111        sw = SoapWriter()
112        soapFault.serialize(sw)
113        environ[cls.soapFaultSetKey] = 'True'
114        return sw
115   
116    pathMatch = lambda self,environ:environ['PATH_INFO']==self.app_conf['path']
117       
118    @staticmethod
119    def isSOAPMessage(environ):
120        '''Generic method to filter out non-soap messages
121       
122        TODO: is HTTP_SOAPACTION only set for WSDL doc-literal wrapped style
123        generated content? - If so this test should be moved'''
124        return environ.get('REQUEST_METHOD', '') == 'POST' and \
125               environ.get('HTTP_SOAPACTION') is not None
126
127    @classmethod
128    def isSOAPFaultSet(cls, environ):
129        '''Check environment for SOAP fault flag set.  This variable is set
130        from exception2SOAPFault'''
131        return bool(environ.get(cls.soapFaultSetKey, False)) == True
132   
133    @classmethod
134    def parseRequest(cls, environ):
135        '''Parse SOAP message from environ['wsgi.input']
136       
137        Reading from environ['wsgi.input'] may be a destructive process so the
138        content is saved in a ZSI.parse.ParsedSoap object for use by SOAP
139        handlers which follow in the chain
140       
141        environ['ZSI.parse.ParsedSoap'] may be set to a ParsedSoap object
142        parsed by a SOAP handler ahead of the current one in the chain.  In
143        this case, don't re-parse.  If NOT parsed, parse and set
144        'ZSI.parse.ParsedSoap' environ key'''
145       
146        # Check for ParsedSoap object set in environment, if not present,
147        # make one
148        ps = environ.get(cls.parsedSOAPKey)
149        if ps is None:
150            # TODO: allow for chunked data
151            contentLength = int(environ['CONTENT_LENGTH'])
152            soapIn = environ['wsgi.input'].read(contentLength)
153            if len(soapIn) < contentLength:
154                raise SOAPMiddlewareReadError("Expecting %s content length; "
155                                              "received %d instead." % \
156                                              (environ['CONTENT_LENGTH'],
157                                               len(soapIn)))
158           
159            log.debug("SOAP Request for handler %r" % cls)
160            log.debug("_"*80)
161            log.debug(soapIn)
162            log.debug("_"*80)
163           
164            ps = ParsedSoap(soapIn)
165            environ[cls.parsedSOAPKey] = ps
166           
167        return environ[cls.parsedSOAPKey]
168
169
170    def writeResponse(self, environ, start_response, errorCode=None):
171        '''This method serializes the SOAP output and sets the response header.
172        It's the final step and should be called in the last SOAP handler in
173        a chain of handlers or else specify it in the ini file as the last
174        SOAP handler'''
175       
176        # This flag must be set to True to write out the final response from
177        # this handler
178        if self.writeResponseSet == False:
179            return self.app(environ, start_response)
180       
181        sw = self.getSOAPWriter(environ)
182        soapOut = str(sw)
183        charset = self.app_conf['charset']
184       
185        if errorCode is None:
186            if self.isSOAPFaultSet(environ):
187                errorCode = "500 Internal Server Error"
188            else:
189                errorCode = "200 OK"
190               
191        start_response(errorCode,
192                       [('content-type', 'text/xml'+charset),
193                        ('content-length', str(len(soapOut)))])
194        return soapOut
195
196    @classmethod
197    def getSOAPWriter(cls, environ):
198        '''Access SoapWriter object set in environment by this classes' call
199        method'''
200       
201        sw = environ.get(SOAPMiddleware.soapWriterKey)
202        if sw is None:
203            raise KeyError("Expecting '%s' key in environ: missing call to "
204                           "SOAPMiddleware?" % SOAPMiddleware.soapWriterKey)
205        return sw
206   
207    @classmethod
208    def setSOAPWriter(cls, environ, sw):
209        '''Set SoapWriter object in environment'''   
210        environ[SOAPMiddleware.soapWriterKey] = sw
211
212    def addFilter2Environ(self, environ):
213        '''Add a key to the current application in the environment so that
214        other middleware can reference it.  This is dependent on filterID set
215        in app_conf'''
216        filterID = self.app_conf.get('filterID')
217        if filterID is not None:           
218            if filterID in environ:
219                raise SOAPMiddlewareConfigError("An filterID key '%s' is "
220                                                "already set in environ" % \
221                                                filterID)
222            environ[filterID] = self
223           
224       
225class SOAPBindingMiddleware(SOAPMiddleware): 
226    '''Apply a ZSI ServiceSOAPBinding type SOAP service'''
227         
228    def __init__(self, *arg, **kw):
229       
230        super(SOAPBindingMiddleware, self).__init__(*arg, **kw)
231       
232        # Check for Service binding in config
233        if 'ServiceSOAPBindingClass' in self.app_conf:
234            modName, dot, className = \
235                    self.app_conf['ServiceSOAPBindingClass'].rpartition('.')
236           
237            self.serviceSOAPBinding = instantiateClass(modName, 
238                                   className, 
239                                   objectType=ServiceSOAPBinding, 
240                                   classProperties=self.serviceSOAPBindingKw)           
241        else: 
242            self.serviceSOAPBinding = ServiceSOAPBinding()
243       
244        # Flag to enable display of WSDL via wsdl query arg in a GET request
245        self.enableWSDLQuery = self.app_conf.get('enableWSDLQuery', False) and\
246                                hasattr(self.serviceSOAPBinding, '_wsdl')
247
248        # Check for a list of other filters to be referenced by this one
249        if 'referencedFilters' in self.app_conf:
250            # __call__ will add any filters found to the service SOAP Binding
251            # - check this doesn't have a name clash with the attribute to be
252            # added
253            if hasattr(self.serviceSOAPBinding, 'referencedWSGIFilters'):
254                raise SOAPMiddlewareConfigError("Service SOAP binding class "
255                                                "%r already has an attribute "
256                                                "'referencedWSGIFilters'" % \
257                                                self.serviceSOAPBinding)
258            self.referencedFilterKeys = \
259                                    self.app_conf['referencedFilters'].split()
260
261    def _getServiceSOAPBindingKw(self):
262        '''Extract keywords to specific to SOAP Service Binding set in app_conf
263        '''
264        if 'ServiceSOAPBindingPropPrefix' not in self.app_conf:
265            return {}
266       
267        prefix = self.app_conf['ServiceSOAPBindingPropPrefix']+'.'
268        serviceSOAPBindingKw = dict([(k.replace(prefix, ''), v) \
269                                     for k,v in self.app_conf.items() \
270                                     if k.startswith(prefix)])
271        return serviceSOAPBindingKw
272   
273    serviceSOAPBindingKw = property(fget=_getServiceSOAPBindingKw)
274   
275    def __call__(self, environ, start_response):
276        log.debug("SOAPBindingMiddleware.__call__ ...")
277               
278        if self.pathMatch(environ) and self.enableWSDLQuery and \
279           environ.get('REQUEST_METHOD') == 'GET' and \
280           environ.get('QUERY_STRING') == 'wsdl':
281            wsdl = self.serviceSOAPBinding._wsdl
282            start_response("200 OK", [('Content-type', 'text/xml'),
283                                      ('Content-length', str(len(wsdl)))])
284            return wsdl
285               
286               
287        # Apply filter for calls
288        response = self._initCall(environ, start_response)
289        if response is not None:
290            return response
291       
292       
293        try:
294            # Other filters in the middleware chain may be passed by setting
295            # a reference to them in the config.  This is useful if the SOAP
296            # binding code needs to access results from upstream middleware
297            # e.g. check output from signature verification filter
298            if hasattr(self, 'referencedFilterKeys'):
299                self.serviceSOAPBinding.referencedWSGIFilters = \
300                                    dict([(i, environ[i]) \
301                                          for i in self.referencedFilterKeys])
302                   
303            ps = self.parseRequest(environ)
304           
305            # Map SOAP Action to method in binding class
306            soapMethodName = 'soap_%s' % environ['HTTP_SOAPACTION'].strip('"')
307           
308            method = getattr(self.serviceSOAPBinding, soapMethodName)
309           
310            # TODO: change method to return response only: request, response
311            # tuple is carry over from Twisted based code
312            req, resp = method(ps)
313        except Exception, e:
314            sw = self.exception2SOAPFault(environ, e)
315        else: 
316            # Serialize output using SOAP writer class
317            sw = SoapWriter()
318            sw.serialize(resp)
319       
320        # Make SoapWriter object available to any SOAP filters that follow
321        self.setSOAPWriter(environ, sw)
322       
323        soapOut = str(sw)
324        charset = self.app_conf['charset']
325               
326        log.debug("SOAP Response")
327        log.debug("_"*80)
328        log.debug(soapOut)
329        log.debug("_"*80)
330
331        return self.writeResponse(environ, start_response)
Note: See TracBrowser for help on using the repository browser.