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

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

Changed saml module into a package to avoid import quirk with conflicting saml namespace from saml egg

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.