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

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

AuthzService? unit test wiht ndg.security.server.wsgi.authzservice.AuthzServiceMiddleware? near complete. Fixes required to PIP callout to Attribute Authority.

Line 
1"""WSGI SAML package for SAML 2.0 Attribute and Authorisation Decision Query/
2Request Profile interfaces
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__)
14import traceback
15from cStringIO import StringIO
16from uuid import uuid4
17from datetime import datetime
18from xml.etree import ElementTree
19
20from ndg.saml.saml2.core import Response, AttributeQuery, Status, StatusCode
21from ndg.saml.xml import UnknownAttrProfile
22from ndg.saml.xml.etree import AttributeQueryElementTree, ResponseElementTree
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.common.utils.factory import importModuleObject
29from ndg.security.server.wsgi import NDGSecurityPathFilter
30from ndg.security.server.wsgi.soap import SOAPMiddleware
31
32
33class SOAPQueryInterfaceMiddlewareError(Exception):
34    """Base class for WSGI SAML 2.0 SOAP Query Interface Errors"""
35
36
37class SOAPQueryInterfaceMiddlewareConfigError(Exception):
38    """WSGI SAML 2.0 SOAP Query Interface Configuration problem"""
39
40 
41class SOAPQueryInterfaceMiddleware(SOAPMiddleware, NDGSecurityPathFilter):
42    """Implementation of SAML 2.0 SOAP Binding for Query/Request Binding
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('SOAPQueryInterfaceMiddleware')
55    PATH_OPTNAME = "pathMatchList"
56    QUERY_INTERFACE_KEYNAME_OPTNAME = "queryInterfaceKeyName"
57    DEFAULT_QUERY_INTERFACE_KEYNAME = ("ndg.security.server.wsgi.saml."
58                            "SOAPQueryInterfaceMiddleware.queryInterface")
59   
60    REQUEST_ENVELOPE_CLASS_OPTNAME = 'requestEnvelopeClass'
61    RESPONSE_ENVELOPE_CLASS_OPTNAME = 'responseEnvelopeClass'
62    SERIALISE_OPTNAME = 'serialise'
63    DESERIALISE_OPTNAME = 'deserialise' 
64     
65    CONFIG_FILE_OPTNAMES = (
66        PATH_OPTNAME,
67        QUERY_INTERFACE_KEYNAME_OPTNAME,
68        DEFAULT_QUERY_INTERFACE_KEYNAME,
69        REQUEST_ENVELOPE_CLASS_OPTNAME,
70        RESPONSE_ENVELOPE_CLASS_OPTNAME,
71        SERIALISE_OPTNAME,
72        DESERIALISE_OPTNAME
73    )
74   
75    def __init__(self, app):
76        '''@type app: callable following WSGI interface
77        @param app: next middleware application in the chain
78        '''     
79        NDGSecurityPathFilter.__init__(self, app, None)
80       
81        self._app = app
82       
83        # Set defaults
84        cls = SOAPQueryInterfaceMiddleware
85        self.__queryInterfaceKeyName = cls.DEFAULT_QUERY_INTERFACE_KEYNAME
86        self.pathMatchList = ['/']
87        self.__requestEnvelopeClass = None
88        self.__responseEnvelopeClass = None
89        self.__serialise = None
90        self.__deserialise = None
91                 
92    def initialise(self, global_conf, prefix='', **app_conf):
93        '''
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        cls = SOAPQueryInterfaceMiddleware
103       
104        # Override where set in config
105        for name in SOAPQueryInterfaceMiddleware.CONFIG_FILE_OPTNAMES:
106            val = app_conf.get(prefix + name)
107            if val is not None:
108                setattr(self, name, val)
109
110        if self.serialise is None:
111            raise AttributeError('No "serialise" method set to serialise the '
112                                 'SAML response from this middleware.')
113
114        if self.deserialise is None:
115            raise AttributeError('No "deserialise" method set to parse the '
116                                 'SAML request to this middleware.')
117           
118    def _getSerialise(self):
119        return self.__serialise
120
121    def _setSerialise(self, value):
122        if isinstance(value, basestring):
123            self.__serialise = importModuleObject(value)
124           
125        elif callable(value):
126            self.__serialise = value
127        else:
128            raise TypeError('Expecting callable for "serialise"; got %r' % 
129                            value)
130
131    serialise = property(_getSerialise, _setSerialise, 
132                         doc="callable to serialise request into XML type")
133
134    def _getDeserialise(self):
135        return self.__deserialise
136
137    def _setDeserialise(self, value):
138        if isinstance(value, basestring):
139            self.__deserialise = importModuleObject(value)
140           
141        elif callable(value):
142            self.__deserialise = value
143        else:
144            raise TypeError('Expecting callable for "deserialise"; got %r' % 
145                            value)
146       
147
148    deserialise = property(_getDeserialise, 
149                           _setDeserialise, 
150                           doc="callable to de-serialise response from XML "
151                               "type")       
152    @classmethod
153    def filter_app_factory(cls, app, global_conf, **app_conf):
154        """Set-up using a Paste app factory pattern.  Set this method to avoid
155        possible conflicts from multiple inheritance
156       
157        @type app: callable following WSGI interface
158        @param app: next middleware application in the chain     
159        @type global_conf: dict       
160        @param global_conf: PasteDeploy global configuration dictionary
161        @type prefix: basestring
162        @param prefix: prefix for configuration items
163        @type app_conf: dict       
164        @param app_conf: PasteDeploy application specific configuration
165        dictionary
166        """
167        app = cls(app)
168        app.initialise(global_conf, **app_conf)
169       
170        return app
171   
172    def _getQueryInterfaceKeyName(self):
173        return self.__queryInterfaceKeyName
174
175    def _setQueryInterfaceKeyName(self, value):
176        if not isinstance(value, basestring):
177            raise TypeError('Expecting string type for "queryInterfaceKeyName"'
178                            ' got %r' % value)
179           
180        self.__queryInterfaceKeyName = value
181
182    queryInterfaceKeyName = property(fget=_getQueryInterfaceKeyName, 
183                                     fset=_setQueryInterfaceKeyName, 
184                                     doc="environ key name for Attribute Query "
185                                         "interface")
186   
187    @NDGSecurityPathFilter.initCall
188    def __call__(self, environ, start_response):
189        """Check for and parse a SOAP SAML Attribute Query and return a
190        SAML Response
191       
192        @type environ: dict
193        @param environ: WSGI environment variables dictionary
194        @type start_response: function
195        @param start_response: standard WSGI start response function
196        """
197   
198        # Ignore non-matching path
199        if not self.pathMatch:
200            return self._app(environ, start_response)
201         
202        # Ignore non-POST requests
203        if environ.get('REQUEST_METHOD') != 'POST':
204            return self._app(environ, start_response)
205       
206        soapRequestStream = environ.get('wsgi.input')
207        if soapRequestStream is None:
208            raise SOAPQueryInterfaceMiddlewareError('No "wsgi.input" in '
209                                                    'environ')
210       
211        # TODO: allow for chunked data
212        contentLength = environ.get('CONTENT_LENGTH')
213        if contentLength is None:
214            raise SOAPQueryInterfaceMiddlewareError('No "CONTENT_LENGTH" in '
215                                                    'environ')
216
217        contentLength = int(contentLength)
218        soapRequestTxt = soapRequestStream.read(contentLength)
219       
220        # Parse into a SOAP envelope object
221        soapRequest = SOAPEnvelope()
222        soapRequest.parse(StringIO(soapRequestTxt))
223       
224        log.debug("SOAPQueryInterfaceMiddleware.__call__: received SAML "
225                  "SOAP AttributeQuery ...")
226       
227        queryElem = soapRequest.body.elem[0]
228       
229        try:
230            query = self.deserialise(queryElem)
231        except UnknownAttrProfile:
232            log.exception("%r raised parsing incoming query: " % 
233                          (type(e), traceback.format_exc()))
234            samlResponse = self._makeErrorResponse(
235                                        StatusCode.UNKNOWN_ATTR_PROFILE_URI)
236        else:   
237            # Check for Query Interface in environ
238            queryInterface = environ.get(self.queryInterfaceKeyName)
239            if queryInterface is None:
240                raise SOAPQueryInterfaceMiddlewareConfigError(
241                                'No query interface "%s" key found in environ' %
242                                self.queryInterfaceKeyName)
243           
244            # Call query interface       
245            samlResponse = queryInterface(query)
246       
247        # Convert to ElementTree representation to enable attachment to SOAP
248        # response body
249        samlResponseElem = self.serialise(samlResponse)
250       
251        # Create SOAP response and attach the SAML Response payload
252        soapResponse = SOAPEnvelope()
253        soapResponse.create()
254        soapResponse.body.elem.append(samlResponseElem)
255       
256        response = soapResponse.serialize()
257       
258        log.debug("SOAPQueryInterfaceMiddleware.__call__: sending response "
259                  "...\n\n%s",
260                  response)
261        start_response("200 OK",
262                       [('Content-length', str(len(response))),
263                        ('Content-type', 'text/xml')])
264        return [response]
265
266    def _makeErrorResponse(self, code):
267        """Convenience method for making a basic response following an error
268        """
269        samlResponse = Response()
270       
271        samlResponse.issueInstant = datetime.utcnow()           
272        samlResponse.id = str(uuid4())
273       
274        # Initialise to success status but reset on error
275        samlResponse.status = Status()
276        samlResponse.status.statusCode = StatusCode()
277        samlResponse.status.statusCode.value = code
278       
279        return samlResponse
280
Note: See TracBrowser for help on using the repository browser.