Ignore:
Timestamp:
16/02/10 16:11:08 (11 years ago)
Author:
pjkersha
Message:
  • Important fix for SOAP client used with SAML SOAP binding: set text/xml content type.
  • Refactored SAML SOAP binding query clients.
File:
1 edited

Legend:

Unmodified
Added
Removed
  • TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/saml/__init__.py

    r6573 r6578  
    99__contact__ = "Philip.Kershaw@stfc.ac.uk" 
    1010__revision__ = "$Id: $" 
    11 __license__ = "BSD - see LICENSE file in top-levle directory" 
     11__license__ = "BSD - see LICENSE file in top-level directory" 
     12import logging 
     13log = logging.getLogger(__name__) 
     14import traceback 
     15from cStringIO import StringIO 
     16from uuid import uuid4 
     17from datetime import datetime 
     18from xml.etree import ElementTree 
     19 
     20from saml.saml2.core import Response, AttributeQuery, Status, StatusCode  
     21from saml.xml import UnknownAttrProfile 
     22from saml.xml.etree import AttributeQueryElementTree, ResponseElementTree 
     23 
     24from ndg.security.common.saml_utils.esg import XSGroupRoleAttributeValue 
     25from ndg.security.common.saml_utils.esg.xml.etree import ( 
     26                                        XSGroupRoleAttributeValueElementTree) 
     27from ndg.security.common.soap.etree import SOAPEnvelope 
     28from ndg.security.common.utils.factory import importModuleObject 
     29from ndg.security.server.wsgi import NDGSecurityPathFilter 
     30from ndg.security.server.wsgi.soap import SOAPMiddleware 
     31 
     32 
     33class SOAPQueryInterfaceMiddlewareError(Exception): 
     34    """Base class for WSGI SAML 2.0 SOAP Query Interface Errors""" 
     35 
     36 
     37class SOAPQueryInterfaceMiddlewareConfigError(Exception): 
     38    """WSGI SAML 2.0 SOAP Query Interface Configuration problem""" 
     39 
     40   
     41class SOAPQueryInterfaceMiddleware(SOAPMiddleware, NDGSecurityPathFilter): 
     42    """Implementation of SAML 2.0 SOAP Binding for Query/Request Binding 
     43     
     44    @type PATH_OPTNAME: basestring 
     45    @cvar PATH_OPTNAME: name of app_conf option for specifying a path or paths 
     46    that this middleware will intercept and process 
     47    @type QUERY_INTERFACE_KEYNAME_OPTNAME: basestring 
     48    @cvar QUERY_INTERFACE_KEYNAME_OPTNAME: app_conf option name for key name 
     49    used to reference the SAML query interface in environ 
     50    @type DEFAULT_QUERY_INTERFACE_KEYNAME: basestring 
     51    @param DEFAULT_QUERY_INTERFACE_KEYNAME: default key name for referencing 
     52    SAML query interface in environ 
     53    """ 
     54    log = logging.getLogger('SOAPQueryInterfaceMiddleware') 
     55    PATH_OPTNAME = "pathMatchList" 
     56    QUERY_INTERFACE_KEYNAME_OPTNAME = "queryInterfaceKeyName" 
     57    DEFAULT_QUERY_INTERFACE_KEYNAME = ("ndg.security.server.wsgi.saml." 
     58                            "SOAPQueryInterfaceMiddleware.queryInterface") 
     59     
     60    REQUEST_ENVELOPE_CLASS_OPTNAME = 'requestEnvelopeClass' 
     61    RESPONSE_ENVELOPE_CLASS_OPTNAME = 'responseEnvelopeClass' 
     62    SERIALISE_OPTNAME = 'serialise' 
     63    DESERIALISE_OPTNAME = 'deserialise'  
     64      
     65    CONFIG_FILE_OPTNAMES = ( 
     66        PATH_OPTNAME, 
     67        QUERY_INTERFACE_KEYNAME_OPTNAME, 
     68        DEFAULT_QUERY_INTERFACE_KEYNAME, 
     69        REQUEST_ENVELOPE_CLASS_OPTNAME, 
     70        RESPONSE_ENVELOPE_CLASS_OPTNAME, 
     71        SERIALISE_OPTNAME, 
     72        DESERIALISE_OPTNAME 
     73    ) 
     74     
     75    def __init__(self, app): 
     76        '''@type app: callable following WSGI interface 
     77        @param app: next middleware application in the chain  
     78        '''      
     79        NDGSecurityPathFilter.__init__(self, app, None) 
     80         
     81        self._app = app 
     82         
     83        # Set defaults 
     84        cls = SOAPQueryInterfaceMiddleware 
     85        self.__queryInterfaceKeyName = cls.DEFAULT_QUERY_INTERFACE_KEYNAME 
     86        self.pathMatchList = ['/'] 
     87        self.__requestEnvelopeClass = None 
     88        self.__responseEnvelopeClass = None 
     89        self.__serialise = None 
     90        self.__deserialise = None 
     91                  
     92    def initialise(self, global_conf, prefix='', **app_conf): 
     93        ''' 
     94        @type global_conf: dict         
     95        @param global_conf: PasteDeploy global configuration dictionary 
     96        @type prefix: basestring 
     97        @param prefix: prefix for configuration items 
     98        @type app_conf: dict         
     99        @param app_conf: PasteDeploy application specific configuration  
     100        dictionary 
     101        ''' 
     102        cls = SOAPQueryInterfaceMiddleware 
     103         
     104        # Override where set in config 
     105        for name in SOAPQueryInterfaceMiddleware.CONFIG_FILE_OPTNAMES: 
     106            val = app_conf.get(prefix + name) 
     107            if val is not None: 
     108                setattr(self, name, val) 
     109 
     110        if self.serialise is None: 
     111            raise AttributeError('No "serialise" method set to serialise the ' 
     112                                 'SAML response from this middleware.') 
     113 
     114        if self.deserialise is None: 
     115            raise AttributeError('No "deserialise" method set to parse the ' 
     116                                 'SAML request to this middleware.') 
     117             
     118    def _getSerialise(self): 
     119        return self.__serialise 
     120 
     121    def _setSerialise(self, value): 
     122        if isinstance(value, basestring): 
     123            self.__serialise = importModuleObject(value) 
     124             
     125        elif callable(value): 
     126            self.__serialise = value 
     127        else: 
     128            raise TypeError('Expecting callable for "serialise"; got %r' %  
     129                            value) 
     130 
     131    serialise = property(_getSerialise, _setSerialise,  
     132                         doc="callable to serialise request into XML type") 
     133 
     134    def _getDeserialise(self): 
     135        return self.__deserialise 
     136 
     137    def _setDeserialise(self, value): 
     138        if isinstance(value, basestring): 
     139            self.__deserialise = importModuleObject(value) 
     140             
     141        elif callable(value): 
     142            self.__deserialise = value 
     143        else: 
     144            raise TypeError('Expecting callable for "deserialise"; got %r' %  
     145                            value) 
     146         
     147 
     148    deserialise = property(_getDeserialise,  
     149                           _setDeserialise,  
     150                           doc="callable to de-serialise response from XML " 
     151                               "type")         
     152    @classmethod 
     153    def filter_app_factory(cls, app, global_conf, **app_conf): 
     154        """Set-up using a Paste app factory pattern.  Set this method to avoid 
     155        possible conflicts from multiple inheritance 
     156         
     157        @type app: callable following WSGI interface 
     158        @param app: next middleware application in the chain       
     159        @type global_conf: dict         
     160        @param global_conf: PasteDeploy global configuration dictionary 
     161        @type prefix: basestring 
     162        @param prefix: prefix for configuration items 
     163        @type app_conf: dict         
     164        @param app_conf: PasteDeploy application specific configuration  
     165        dictionary 
     166        """ 
     167        app = cls(app) 
     168        app.initialise(global_conf, **app_conf) 
     169         
     170        return app 
     171     
     172    def _getQueryInterfaceKeyName(self): 
     173        return self.__queryInterfaceKeyName 
     174 
     175    def _setQueryInterfaceKeyName(self, value): 
     176        if not isinstance(value, basestring): 
     177            raise TypeError('Expecting string type for "queryInterfaceKeyName"' 
     178                            ' got %r' % value) 
     179             
     180        self.__queryInterfaceKeyName = value 
     181 
     182    queryInterfaceKeyName = property(fget=_getQueryInterfaceKeyName,  
     183                                     fset=_setQueryInterfaceKeyName,  
     184                                     doc="environ keyname for Attribute Query " 
     185                                         "interface") 
     186     
     187    @NDGSecurityPathFilter.initCall 
     188    def __call__(self, environ, start_response): 
     189        """Check for and parse a SOAP SAML Attribute Query and return a 
     190        SAML Response 
     191         
     192        @type environ: dict 
     193        @param environ: WSGI environment variables dictionary 
     194        @type start_response: function 
     195        @param start_response: standard WSGI start response function 
     196        """ 
     197     
     198        # Ignore non-matching path 
     199        if not self.pathMatch: 
     200            return self._app(environ, start_response) 
     201           
     202        # Ignore non-POST requests 
     203        if environ.get('REQUEST_METHOD') != 'POST': 
     204            return self._app(environ, start_response) 
     205         
     206        soapRequestStream = environ.get('wsgi.input') 
     207        if soapRequestStream is None: 
     208            raise SOAPQueryInterfaceMiddlewareError('No "wsgi.input" in ' 
     209                                                    'environ') 
     210         
     211        # TODO: allow for chunked data 
     212        contentLength = environ.get('CONTENT_LENGTH') 
     213        if contentLength is None: 
     214            raise SOAPQueryInterfaceMiddlewareError('No "CONTENT_LENGTH" in ' 
     215                                                    'environ') 
     216 
     217        contentLength = int(contentLength)         
     218        soapRequestTxt = soapRequestStream.read(contentLength) 
     219         
     220        # Parse into a SOAP envelope object 
     221        soapRequest = SOAPEnvelope() 
     222        soapRequest.parse(StringIO(soapRequestTxt)) 
     223         
     224        log.debug("SOAPQueryInterfaceMiddleware.__call__: received SAML " 
     225                  "SOAP AttributeQuery ...") 
     226        
     227        queryElem = soapRequest.body.elem[0] 
     228         
     229        try: 
     230            query = self.deserialise(queryElem) 
     231        except UnknownAttrProfile: 
     232            log.exception("%r raised parsing incoming query: " %  
     233                          (type(e), traceback.format_exc())) 
     234            samlResponse = self._makeErrorResponse( 
     235                                        StatusCode.UNKNOWN_ATTR_PROFILE_URI) 
     236        else:    
     237            # Check for Query Interface in environ 
     238            queryInterface = environ.get(self.queryInterfaceKeyName) 
     239            if queryInterface is None: 
     240                raise SOAPQueryInterfaceMiddlewareConfigError( 
     241                                'No query interface "%s" key found in environ' % 
     242                                self.queryInterfaceKeyName) 
     243             
     244            # Call query interface         
     245            samlResponse = queryInterface(query) 
     246         
     247        # Convert to ElementTree representation to enable attachment to SOAP 
     248        # response body 
     249        samlResponseElem = self.serialise(samlResponse) 
     250         
     251        # Create SOAP response and attach the SAML Response payload 
     252        soapResponse = SOAPEnvelope() 
     253        soapResponse.create() 
     254        soapResponse.body.elem.append(samlResponseElem) 
     255         
     256        response = soapResponse.serialize() 
     257         
     258        log.debug("SOAPQueryInterfaceMiddleware.__call__: sending response " 
     259                  "...\n\n%s", 
     260                  response) 
     261        start_response("200 OK", 
     262                       [('Content-length', str(len(response))), 
     263                        ('Content-type', 'text/xml')]) 
     264        return [response] 
     265 
     266    def _makeErrorResponse(self, code): 
     267        """Convenience method for making a basic response following an error 
     268        """ 
     269        samlResponse = Response() 
     270         
     271        samlResponse.issueInstant = datetime.utcnow()             
     272        samlResponse.id = str(uuid4()) 
     273         
     274        # Initialise to success status but reset on error 
     275        samlResponse.status = Status() 
     276        samlResponse.status.statusCode = StatusCode() 
     277        samlResponse.status.statusCode.value = code 
     278         
     279        return samlResponse 
     280 
Note: See TracChangeset for help on using the changeset viewer.