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

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

Updated BaseSignatureHandler? and WSSecurityconfig classes to correctly handle config via keywords:

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