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

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

Integrated SOAP SAML Attribute Query interface into Attribute Authority Client unit tests.

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 xml.etree import ElementTree
16
17from saml.saml2.core import Response, Assertion, Attribute, AttributeValue, \
18    AttributeStatement, SAMLVersion, Subject, NameID, Issuer, AttributeQuery, \
19    XSStringAttributeValue, XSGroupRoleAttributeValue, Conditions, Status, \
20    StatusCode
21   
22from saml.common.xml import SAMLConstants
23from saml.xml.etree import AssertionElementTree, AttributeQueryElementTree, \
24    ResponseElementTree, XSGroupRoleAttributeValueElementTree, QName
25
26from ndg.security.common.soap.etree import SOAPEnvelope
27from ndg.security.common.utils.etree import prettyPrint
28from ndg.security.server.wsgi import NDGSecurityPathFilter
29from ndg.security.server.wsgi.soap import SOAPMiddleware
30
31class SOAPAttributeInterfaceMiddlewareError(Exception):
32    """Base class for WSGI SAML 2.0 SOAP Attribute Interface Errors"""
33
34
35class SOAPAttributeInterfaceMiddlewareConfigError(Exception):
36    """WSGI SAML 2.0 SOAP Attribute Interface Configuration problem"""
37
38 
39class SOAPAttributeInterfaceMiddleware(SOAPMiddleware, NDGSecurityPathFilter):
40    """Implementation of SAML 2.0 SOAP Binding for Assertion Query/Request
41    Profile
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        start_response("200 OK",
220                       [('Content-length', str(len(response))),
221                        ('Content-type', 'text/xml')])
222        return [response]
223   
224    @classmethod
225    def isAttributeQuery(cls, soapBody):
226        """Check for AttributeQuery in the SOAP Body"""
227       
228        if len(soapBody.elem) != 1:
229            # TODO: Change to a SOAP Fault?
230            raise SOAPAttributeInterfaceMiddlewareError("Expecting single "
231                                                        "child element in the "
232                                                        "request SOAP "
233                                                        "Envelope body")
234           
235        inputQName = QName(soapBody.elem[0].tag)   
236        attributeQueryQName = QName.fromGeneric(
237                                        AttributeQuery.DEFAULT_ELEMENT_NAME)
238        return inputQName == attributeQueryQName
239
240    def _makeErrorResponse(self, code):
241        """Convenience method for making a basic response following an error
242        """
243        samlResponse = Response()
244       
245        samlResponse.issueInstant = datetime.utcnow()           
246        samlResponse.id = str(uuid4())
247       
248        # Initialise to success status but reset on error
249        samlResponse.status = Status()
250        samlResponse.status.statusCode = StatusCode()
251        samlResponse.status.statusCode.value = code
252       
253        return samlResponse
Note: See TracBrowser for help on using the repository browser.