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

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

Combined Services tests:

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