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