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

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@6575
Revision 6575, 10.4 KB checked in by pjkersha, 11 years ago (diff)

Changes for addition of AuthzDecisionQuery? WSGI interface (Authorisation service)

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