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@4480
Revision 4480, 13.2 KB checked in by pjkersha, 12 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.
RevLine 
[4020]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"
[4404]11__contact__ = "Philip.Kershaw@stfc.ac.uk"
[4020]12__revision__ = "$Id$"
13import logging
14log = logging.getLogger(__name__)
15
[4152]16import paste.request
[4020]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
[4245]23from ndg.security.common.utils.ClassFactory import instantiateClass
24     
[4159]25class SOAPMiddlewareError(Exception):
[4233]26    """Base error handling exception class for the SOAP WSGI middleware module
27    """
[4159]28   
29class SOAPMiddlewareReadError(SOAPMiddlewareError):
[4233]30    """SOAP Middleware read error"""
31
32class SOAPMiddlewareConfigError(SOAPMiddlewareError):
33    """SOAP Middleware configuration error"""
34
[4020]35class SOAPMiddleware(object):
36    '''Middleware configurable to a given ZSI SOAP binding''' 
37   
[4171]38    soapWriterKey = 'ZSI.writer.SoapWriter'
39    parsedSOAPKey = 'ZSI.parse.ParsedSoap'
[4185]40    soapFaultSetKey = 'ndg.security.server.wsgi.soap.soapFault'
[4171]41   
[4020]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)
[4171]47       
48        if 'charset' in self.app_conf:
[4185]49            self.app_conf['charset'] = '; charset=' + self.app_conf['charset']
[4171]50        else:
51            self.app_conf['charset'] = '; charset=utf-8'
52
[4185]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:
[4152]57            self.app_conf['path'] = '/'
[4185]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
[4254]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()
[4185]69
[4020]70    def __call__(self, environ, start_response):
71        log.debug("SOAPMiddleware.__call__")
[4185]72                       
[4233]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):
[4480]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       
[4152]93        # Apply filter for calls
[4185]94        if not self.isSOAPMessage(environ):
95            log.debug("SOAPMiddleware.__call__: skipping non-SOAP call")
[4152]96            return self.app(environ, start_response)
[4185]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)
[4020]110
[4233]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)
[4020]118       
[4233]119        # Return None to __call__ to indicate that it can proceed with
120        # processing the input
121        return None
[4171]122
[4185]123    @classmethod
124    def exception2SOAPFault(cls, environ, exception):
[4171]125        '''Convert an exception into a SOAP fault message'''
[4185]126        soapFault = fault.FaultFromException(exception, None)
[4171]127        sw = SoapWriter()
128        soapFault.serialize(sw)
[4185]129        environ[cls.soapFaultSetKey] = 'True'
[4171]130        return sw
131   
[4185]132    pathMatch = lambda self,environ:environ['PATH_INFO']==self.app_conf['path']
[4020]133       
[4172]134    @staticmethod
[4185]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'''
[4152]140        return environ.get('REQUEST_METHOD', '') == 'POST' and \
141               environ.get('HTTP_SOAPACTION') is not None
[4185]142
[4159]143    @classmethod
[4185]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):
[4159]151        '''Parse SOAP message from environ['wsgi.input']
[4152]152       
[4159]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
[4172]164        ps = environ.get(cls.parsedSOAPKey)
[4159]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:
[4290]170                raise SOAPMiddlewareReadError("Expecting %d content length; "
171                                              "received %d instead." % 
172                                              (contentLength, len(soapIn)))
[4159]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)
[4172]180            environ[cls.parsedSOAPKey] = ps
[4159]181           
[4172]182        return environ[cls.parsedSOAPKey]
[4185]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
[4171]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   
[4185]222    @classmethod
223    def setSOAPWriter(cls, environ, sw):
224        '''Set SoapWriter object in environment'''   
225        environ[SOAPMiddleware.soapWriterKey] = sw
[4172]226
[4233]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 "
[4290]235                                                "already set in environ" %
[4233]236                                                filterID)
237            environ[filterID] = self
238           
239       
[4172]240class SOAPBindingMiddleware(SOAPMiddleware): 
241    '''Apply a ZSI ServiceSOAPBinding type SOAP service'''
242         
243    def __init__(self, *arg, **kw):
[4245]244       
[4185]245        super(SOAPBindingMiddleware, self).__init__(*arg, **kw)
[4172]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           
[4245]252            self.serviceSOAPBinding = instantiateClass(modName, 
253                                   className, 
254                                   objectType=ServiceSOAPBinding, 
255                                   classProperties=self.serviceSOAPBindingKw)           
[4172]256        else: 
[4245]257            self.serviceSOAPBinding = ServiceSOAPBinding()
[4172]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
[4245]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   
[4172]278    def __call__(self, environ, start_response):
[4233]279        log.debug("SOAPBindingMiddleware.__call__ ...")
[4185]280               
[4233]281        if self.pathMatch(environ) and self.enableWSDLQuery and \
282           environ.get('REQUEST_METHOD') == 'GET' and \
[4172]283           environ.get('QUERY_STRING') == 'wsdl':
[4233]284            wsdl = self.serviceSOAPBinding._wsdl
285            start_response("200 OK", [('Content-type', 'text/xml'),
286                                      ('Content-length', str(len(wsdl)))])
287            return wsdl
[4172]288               
289               
290        # Apply filter for calls
[4233]291        response = self._initCall(environ, start_response)
292        if response is not None:
293            return response
294       
295       
[4172]296        try:
[4233]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'):
[4480]302                try:
303                    self.serviceSOAPBinding.referencedWSGIFilters = \
304                                    dict([(i, environ[i]) 
[4233]305                                          for i in self.referencedFilterKeys])
[4480]306                except KeyError:
307                    raise SOAPMiddlewareConfigError('No filter ID "%s" found '
308                                                    'in environ' % i)   
[4185]309            ps = self.parseRequest(environ)
[4172]310           
311            # Map SOAP Action to method in binding class
312            soapMethodName = 'soap_%s' % environ['HTTP_SOAPACTION'].strip('"')
313           
[4285]314            method = getattr(self.serviceSOAPBinding, soapMethodName)           
315            resp = method(ps)
[4172]316        except Exception, e:
[4185]317            sw = self.exception2SOAPFault(environ, e)
[4172]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
[4185]324        self.setSOAPWriter(environ, sw)
[4172]325       
[4185]326        soapOut = str(sw)
327        charset = self.app_conf['charset']
328               
[4172]329        log.debug("SOAP Response")
330        log.debug("_"*80)
[4185]331        log.debug(soapOut)
[4172]332        log.debug("_"*80)
333
[4185]334        return self.writeResponse(environ, start_response)
Note: See TracBrowser for help on using the repository browser.