Changeset 6573


Ignore:
Timestamp:
15/02/10 11:59:33 (9 years ago)
Author:
pjkersha
Message:

Refactor ndg.security.server.wsgi.saml into separate attribute and authorisation decision statement query interfaces.

Location:
TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/saml
Files:
2 added
1 edited

Legend:

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

    r6512 r6573  
    1 """WSGI SAML Module for SAML 2.0 Assertion Query/Request Profile implementation 
     1"""WSGI SAML package for SAML 2.0 Attribute and Authorisation Decision Query/ 
     2Request Profile interfaces 
    23 
    34NERC DataGrid Project 
    45""" 
    56__author__ = "P J Kershaw" 
    6 __date__ = "17/08/2009" 
    7 __copyright__ = "(C) 2009 Science and Technology Facilities Council" 
     7__date__ = "15/02/10" 
     8__copyright__ = "(C) 2010 Science and Technology Facilities Council" 
    89__contact__ = "Philip.Kershaw@stfc.ac.uk" 
    910__revision__ = "$Id: $" 
    1011__license__ = "BSD - see LICENSE file in top-levle directory" 
    11 import logging 
    12 log = logging.getLogger(__name__) 
    13 from cStringIO import StringIO 
    14 from uuid import uuid4 
    15 from datetime import datetime 
    16 from xml.etree import ElementTree 
    17  
    18 from saml.saml2.core import (Response, Assertion, Attribute, AttributeValue,  
    19                              AttributeStatement, SAMLVersion, Subject, NameID,  
    20                              Issuer, AttributeQuery, XSStringAttributeValue,  
    21                              Conditions, Status, StatusCode) 
    22      
    23 from saml.common.xml import SAMLConstants 
    24 from saml.xml import UnknownAttrProfile 
    25 from saml.xml.etree import (AssertionElementTree, AttributeQueryElementTree,  
    26                             ResponseElementTree, QName) 
    27  
    28 from ndg.security.common.saml_utils.esg import XSGroupRoleAttributeValue 
    29 from ndg.security.common.saml_utils.esg.xml.etree import ( 
    30                                         XSGroupRoleAttributeValueElementTree) 
    31 from ndg.security.common.soap.etree import SOAPEnvelope 
    32 from ndg.security.common.utils.etree import prettyPrint 
    33 from ndg.security.server.wsgi import NDGSecurityPathFilter 
    34 from ndg.security.server.wsgi.soap import SOAPMiddleware 
    35  
    36  
    37 class SOAPAttributeInterfaceMiddlewareError(Exception): 
    38     """Base class for WSGI SAML 2.0 SOAP Attribute Interface Errors""" 
    39  
    40  
    41 class SOAPAttributeInterfaceMiddlewareConfigError(Exception): 
    42     """WSGI SAML 2.0 SOAP Attribute Interface Configuration problem""" 
    43  
    44    
    45 class SOAPAttributeInterfaceMiddleware(SOAPMiddleware, NDGSecurityPathFilter): 
    46     """Implementation of SAML 2.0 SOAP Binding for Assertion Query/Request 
    47     Profile 
    48      
    49     @type PATH_OPTNAME: basestring 
    50     @cvar PATH_OPTNAME: name of app_conf option for specifying a path or paths 
    51     that this middleware will intercept and process 
    52     @type QUERY_INTERFACE_KEYNAME_OPTNAME: basestring 
    53     @cvar QUERY_INTERFACE_KEYNAME_OPTNAME: app_conf option name for key name 
    54     used to reference the SAML query interface in environ 
    55     @type DEFAULT_QUERY_INTERFACE_KEYNAME: basestring 
    56     @param DEFAULT_QUERY_INTERFACE_KEYNAME: default key name for referencing 
    57     SAML query interface in environ 
    58     """ 
    59     log = logging.getLogger('SOAPAttributeInterfaceMiddleware') 
    60     PATH_OPTNAME = "pathMatchList" 
    61     QUERY_INTERFACE_KEYNAME_OPTNAME = "queryInterfaceKeyName" 
    62     DEFAULT_QUERY_INTERFACE_KEYNAME = ("ndg.security.server.wsgi.saml." 
    63                             "SOAPAttributeInterfaceMiddleware.queryInterface") 
    64      
    65     def __init__(self, app): 
    66         '''@type app: callable following WSGI interface 
    67         @param app: next middleware application in the chain  
    68         '''      
    69         NDGSecurityPathFilter.__init__(self, app, None) 
    70          
    71         self._app = app 
    72                   
    73     def initialise(self, global_conf, prefix='', **app_conf): 
    74         ''' 
    75         @type global_conf: dict         
    76         @param global_conf: PasteDeploy global configuration dictionary 
    77         @type prefix: basestring 
    78         @param prefix: prefix for configuration items 
    79         @type app_conf: dict         
    80         @param app_conf: PasteDeploy application specific configuration  
    81         dictionary 
    82         ''' 
    83         self.__queryInterfaceKeyName = None 
    84          
    85         self.pathMatchList = app_conf.get( 
    86             prefix + SOAPAttributeInterfaceMiddleware.PATH_OPTNAME, ['/']) 
    87                     
    88         self.queryInterfaceKeyName = app_conf.get(prefix + \ 
    89             SOAPAttributeInterfaceMiddleware.QUERY_INTERFACE_KEYNAME_OPTNAME, 
    90             prefix + \ 
    91             SOAPAttributeInterfaceMiddleware.DEFAULT_QUERY_INTERFACE_KEYNAME) 
    92          
    93     @classmethod 
    94     def filter_app_factory(cls, app, global_conf, **app_conf): 
    95         """Set-up using a Paste app factory pattern.  Set this method to avoid 
    96         possible conflicts from multiple inheritance 
    97          
    98         @type app: callable following WSGI interface 
    99         @param app: next middleware application in the chain       
    100         @type global_conf: dict         
    101         @param global_conf: PasteDeploy global configuration dictionary 
    102         @type prefix: basestring 
    103         @param prefix: prefix for configuration items 
    104         @type app_conf: dict         
    105         @param app_conf: PasteDeploy application specific configuration  
    106         dictionary 
    107         """ 
    108         app = cls(app) 
    109         app.initialise(global_conf, **app_conf) 
    110          
    111         return app 
    112      
    113     def _getQueryInterfaceKeyName(self): 
    114         return self.__queryInterfaceKeyName 
    115  
    116     def _setQueryInterfaceKeyName(self, value): 
    117         if not isinstance(value, basestring): 
    118             raise TypeError('Expecting string type for "queryInterfaceKeyName"' 
    119                             ' got %r' % value) 
    120              
    121         self.__queryInterfaceKeyName = value 
    122  
    123     queryInterfaceKeyName = property(fget=_getQueryInterfaceKeyName,  
    124                                      fset=_setQueryInterfaceKeyName,  
    125                                      doc="environ keyname for Attribute Query " 
    126                                          "interface") 
    127  
    128     def _getIssuerName(self): 
    129         return self.__issuerName 
    130  
    131     def _setIssuerName(self, value): 
    132         self.__issuerName = value 
    133  
    134     issuerName = property(fget=_getIssuerName,  
    135                           fset=_setIssuerName,  
    136                           doc="Name of assertion issuing authority") 
    137      
    138     @NDGSecurityPathFilter.initCall 
    139     def __call__(self, environ, start_response): 
    140         """Check for and parse a SOAP SAML Attribute Query and return a 
    141         SAML Response 
    142          
    143         @type environ: dict 
    144         @param environ: WSGI environment variables dictionary 
    145         @type start_response: function 
    146         @param start_response: standard WSGI start response function 
    147         """ 
    148          
    149         # Ignore non-matching path 
    150         if not self.pathMatch: 
    151             return self._app(environ, start_response) 
    152            
    153         # Ignore non-POST requests 
    154         if environ.get('REQUEST_METHOD') != 'POST': 
    155             return self._app(environ, start_response) 
    156          
    157         soapRequestStream = environ.get('wsgi.input') 
    158         if soapRequestStream is None: 
    159             raise SOAPAttributeInterfaceMiddlewareError('No "wsgi.input" in ' 
    160                                                         'environ') 
    161          
    162         # TODO: allow for chunked data 
    163         contentLength = environ.get('CONTENT_LENGTH') 
    164         if contentLength is None: 
    165             raise SOAPAttributeInterfaceMiddlewareError('No "CONTENT_LENGTH" ' 
    166                                                         'in environ') 
    167  
    168         contentLength = int(contentLength)         
    169         soapRequestTxt = soapRequestStream.read(contentLength) 
    170          
    171         # Parse into a SOAP envelope object 
    172         soapRequest = SOAPEnvelope() 
    173         soapRequest.parse(StringIO(soapRequestTxt)) 
    174          
    175         # Filter based on SOAP Body content - expecting an AttributeQuery 
    176         # element 
    177         if not SOAPAttributeInterfaceMiddleware.isAttributeQuery( 
    178                                                             soapRequest.body): 
    179             # Reset wsgi.input for middleware and app downstream 
    180             environ['wsgi.input'] = StringIO(soapRequestTxt) 
    181             return self._app(environ, start_response) 
    182          
    183         log.debug("SOAPAttributeInterfaceMiddleware.__call__: received SAML " 
    184                   "SOAP AttributeQuery ...") 
    185         
    186         attributeQueryElem = soapRequest.body.elem[0] 
    187          
    188         try: 
    189             attributeQuery = AttributeQueryElementTree.fromXML( 
    190                                                             attributeQueryElem) 
    191         except UnknownAttrProfile, e: 
    192             log.exception("Parsing incoming attribute query: " % e) 
    193             samlResponse = self._makeErrorResponse( 
    194                                         StatusCode.UNKNOWN_ATTR_PROFILE_URI) 
    195         else:    
    196             # Check for Query Interface in environ 
    197             queryInterface = environ.get(self.queryInterfaceKeyName) 
    198             if queryInterface is None: 
    199                 raise SOAPAttributeInterfaceMiddlewareConfigError( 
    200                                 'No query interface "%s" key found in environ'% 
    201                                 self.queryInterfaceKeyName) 
    202              
    203             # Call query interface         
    204             samlResponse = queryInterface(attributeQuery) 
    205          
    206         # Add mapping for ESG Group/Role Attribute Value to enable ElementTree 
    207         # Attribute Value factory to render the XML output 
    208         toXMLTypeMap = { 
    209             XSGroupRoleAttributeValue: XSGroupRoleAttributeValueElementTree 
    210         } 
    211          
    212         # Convert to ElementTree representation to enable attachment to SOAP 
    213         # response body 
    214         samlResponseElem = ResponseElementTree.toXML(samlResponse, 
    215                                             customToXMLTypeMap=toXMLTypeMap) 
    216         xml = ElementTree.tostring(samlResponseElem) 
    217          
    218         # Create SOAP response and attach the SAML Response payload 
    219         soapResponse = SOAPEnvelope() 
    220         soapResponse.create() 
    221         soapResponse.body.elem.append(samlResponseElem) 
    222          
    223         response = soapResponse.serialize() 
    224          
    225         log.debug("SOAPAttributeInterfaceMiddleware.__call__: sending response " 
    226                   "...\n\n%s", 
    227                   response) 
    228         start_response("200 OK", 
    229                        [('Content-length', str(len(response))), 
    230                         ('Content-type', 'text/xml')]) 
    231         return [response] 
    232      
    233     @classmethod 
    234     def isAttributeQuery(cls, soapBody): 
    235         """Check for AttributeQuery in the SOAP Body""" 
    236          
    237         if len(soapBody.elem) != 1: 
    238             # TODO: Change to a SOAP Fault? 
    239             raise SOAPAttributeInterfaceMiddlewareError("Expecting single " 
    240                                                         "child element in the " 
    241                                                         "request SOAP " 
    242                                                         "Envelope body") 
    243              
    244         inputQName = QName(soapBody.elem[0].tag)     
    245         attributeQueryQName = QName.fromGeneric( 
    246                                         AttributeQuery.DEFAULT_ELEMENT_NAME) 
    247         return inputQName == attributeQueryQName 
    248  
    249     def _makeErrorResponse(self, code): 
    250         """Convenience method for making a basic response following an error 
    251         """ 
    252         samlResponse = Response() 
    253          
    254         samlResponse.issueInstant = datetime.utcnow()             
    255         samlResponse.id = str(uuid4()) 
    256          
    257         # Initialise to success status but reset on error 
    258         samlResponse.status = Status() 
    259         samlResponse.status.statusCode = StatusCode() 
    260         samlResponse.status.statusCode.value = code 
    261          
    262         return samlResponse 
Note: See TracChangeset for help on using the changeset viewer.