source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/saml.py @ 5657

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/saml.py@5657
Revision 5657, 8.4 KB checked in by pjkersha, 12 years ago (diff)

Preparing saml SOAP Attribute Interface middleware for unit tests.

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