source: TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/wsgi/saml/__init__.py @ 6633

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/wsgi/saml/__init__.py@6633
Revision 6633, 10.8 KB checked in by pjkersha, 9 years ago (diff)

Merging in changes from 6557

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 datetime import datetime
16from xml.etree import ElementTree
17
18from saml.saml2.core import (Response, Assertion, Attribute, AttributeValue, 
19                             AttributeStatement, SAMLVersion, Subject, NameID, 
20                             Issuer, AttributeQuery, XSStringAttributeValue, 
21                             Conditions, Status, StatusCode)
22   
23from saml.common.xml import SAMLConstants
24from saml.xml import UnknownAttrProfile
25from saml.xml.etree import (AssertionElementTree, AttributeQueryElementTree, 
26                            ResponseElementTree, QName)
27
28from ndg.security.common.saml_utils.esg import XSGroupRoleAttributeValue
29from ndg.security.common.saml_utils.esg.xml.etree import (
30                                        XSGroupRoleAttributeValueElementTree)
31from ndg.security.common.soap.etree import SOAPEnvelope
32from ndg.security.common.utils.etree import prettyPrint
33from ndg.security.server.wsgi import NDGSecurityPathFilter
34from ndg.security.server.wsgi.soap import SOAPMiddleware
35
36
37class SOAPAttributeInterfaceMiddlewareError(Exception):
38    """Base class for WSGI SAML 2.0 SOAP Attribute Interface Errors"""
39
40
41class SOAPAttributeInterfaceMiddlewareConfigError(Exception):
42    """WSGI SAML 2.0 SOAP Attribute Interface Configuration problem"""
43
44 
45class 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 TracBrowser for help on using the repository browser.