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

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

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

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