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

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

Refactoring Attribute Authority for inclusion of SAML attribute query interface.

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       
261        # Check for Service binding in config
262        if 'ServiceSOAPBindingClass' in self.app_conf:
263            modName, className = \
264                        self.app_conf['ServiceSOAPBindingClass'].rsplit('.', 1)
265
266            self.serviceSOAPBinding = instantiateClass(modName,
267                                   className,
268                                   objectType=ServiceSOAPBinding,
269                                   classProperties=self.serviceSOAPBindingKw)           
270        else: 
271            self.serviceSOAPBinding = ServiceSOAPBinding()
272       
273        # Flag to enable display of WSDL via wsdl query arg in a GET request
274        self.enableWSDLQuery = self.app_conf.get('enableWSDLQuery', False) and\
275                                hasattr(self.serviceSOAPBinding, '_wsdl')
276
277
278    def _getServiceSOAPBindingKw(self):
279        '''Extract keywords to specific to SOAP Service Binding set in app_conf
280        '''
281        if 'ServiceSOAPBindingPropPrefix' not in self.app_conf:
282            return {}
283       
284        prefix = self.app_conf['ServiceSOAPBindingPropPrefix'] + '.'
285        serviceSOAPBindingKw = dict([(k.replace(prefix, ''), v) \
286                                     for k, v in self.app_conf.items() \
287                                     if k.startswith(prefix)])
288        return serviceSOAPBindingKw
289   
290    serviceSOAPBindingKw = property(fget=_getServiceSOAPBindingKw)
291   
292    def __call__(self, environ, start_response):
293        log.debug("SOAPBindingMiddleware.__call__ ...")
294               
295        if self.pathMatch(environ) and self.enableWSDLQuery and \
296           environ.get('REQUEST_METHOD', '') == 'GET' and \
297           environ.get('QUERY_STRING', '') == 'wsdl':
298            wsdl = self.serviceSOAPBinding._wsdl
299            start_response("200 OK", [('Content-type', 'text/xml'),
300                                      ('Content-length', str(len(wsdl)))])
301            return wsdl
302               
303        # Apply filter for calls
304        response = self._initCall(environ, start_response)
305        if response is not None:
306            return response
307               
308        try:
309            # Other filters in the middleware chain may be passed by setting
310            # a reference to them in the config.  This is useful if the SOAP
311            # binding code needs to access results from upstream middleware
312            # e.g. check output from signature verification filter
313            if hasattr(self, 'referencedFilterKeys'):
314                try:
315                    self.serviceSOAPBinding.referencedWSGIFilters = \
316                                    dict([(i, environ[i]) 
317                                          for i in self.referencedFilterKeys])
318                except KeyError:
319                    raise ZSIMiddlewareConfigError('No filter ID "%s" found '
320                                                    'in environ' % i)   
321            ps = self.parseRequest(environ)
322           
323            # Map SOAP Action to method in binding class
324            soapMethodName = 'soap_%s' % environ['HTTP_SOAPACTION'].strip('"')
325           
326            method = getattr(self.serviceSOAPBinding, soapMethodName)           
327            resp = method(ps)
328        except Exception, e:
329            sw = self.exception2SOAPFault(environ, e)
330        else: 
331            # Serialize output using SOAP writer class
332            sw = SoapWriter(outputclass=self.writerclass)
333            sw.serialize(resp)
334       
335        # Make SoapWriter object available to any SOAP filters that follow
336        self.setSOAPWriter(environ, sw)
337       
338        soapOut = str(sw)
339        charset = self.app_conf['charset']
340
341        return self.writeResponse(environ, start_response)
Note: See TracBrowser for help on using the repository browser.