source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/saml.py @ 5637

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

Refactoring Attribute Authority for inclusion of SAML attribute query interface.

RevLine 
[5623]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.xml import SAMLConstants
23from saml.xml.etree import AssertionElementTree, AttributeQueryElementTree, \
24    ResponseElementTree, XSGroupRoleAttributeValueElementTree
25
26from ndg.security.common.soap.etree import SOAPEnvelope
27from ndg.security.common.utils.etree import QName, prettyPrint
28from ndg.security.server.wsgi.soap import SOAPMiddleware
29
30class SOAPAttributeInterfaceMiddlewareError(Exception):
31    """Base class for WSGI SAML 2.0 SOAP Attribute Interface Errors"""
32   
33class SOAPAttributeInterfaceMiddleware(SOAPMiddleware):
34    """Implementation of SAML 2.0 SOAP Binding for Assertion Query/Request
35    Profile"""
36   
37    def __init__(self, app, global_conf, prefix='', **app_conf):
38        '''
39        @type app: callable following WSGI interface
40        @param app: next middleware application in the chain     
41        @type global_conf: dict       
42        @param global_conf: PasteDeploy global configuration dictionary
43        @type prefix: basestring
44        @param prefix: prefix for configuration items
45        @type app_conf: dict       
46        @param app_conf: PasteDeploy application specific configuration
47        dictionary
48        '''
49        self._app = app
50        self.__assertionLifetime = None
51        self.__issuerName = None
52
53    def _getAssertionLifetime(self):
54        return self.__assertionLifetime
55
56    def _setAssertionLifetime(self, value):
57        self.__assertionLifetime = value
58
59    assertionLifetime = property(fget=_getAssertionLifetime, 
60                                 fset=_setAssertionLifetime, 
61                                 doc="Validity lifetime (seconds) for "
62                                     "assertion issued in a response")
63
64    def _getIssuerName(self):
65        return self.__issuerName
66
67    def _setIssuerName(self, value):
68        self.__issuerName = value
69
70    issuerName = property(fget=_getIssuerName, 
71                          fset=_setIssuerName, 
72                          doc="Name of assertion issuing authority")
73       
74    def __call__(self, environ, start_response):
75        """Check for and parse a SOAP SAML Attribute Query and return a
76        SAML Response
77       
78        @type environ: dict
79        @param environ: WSGI environment variables dictionary
80        @type start_response: function
81        @param start_response: standard WSGI start response function
82        """
83       
84        # Ignore non-SOAP requests
85        if not self.isSOAPMessage(environ):
86            return self._app(environ, start_response)
87       
88        soapRequestStream = environ.get('wsgi.input')
89        if soapRequestStream is None:
90            raise SOAPAttributeInterfaceMiddlewareError('No "wsgi.input" in '
91                                                        'environ')
92       
[5637]93        # TODO: allow for chunked data
94        contentLength = environ.get('CONTENT_LENGTH')
95        if contentLength is None:
96            raise SOAPAttributeInterfaceMiddlewareError('No "CONTENT_LENGTH" '
97                                                        'in environ')
98
99        contentLength = int(contentLength)       
100        soapRequestTxt = soapRequestStream.read(contentLength)
101       
102        # Parse into a SOAP envelope object
[5623]103        soapRequest = SOAPEnvelope()
[5637]104        soapRequest.parse(StringIO(soapRequestTxt))
[5623]105       
[5637]106        # Filter based on SOAP Body content - expecting an AttributeQuery
107        # element
[5623]108        if not SOAPAttributeInterfaceMiddleware.isAttributeQuery(
109                                                            soapRequest.body):
110            # Reset wsgi.input for middleware and app downstream
111            environ['wsgi.input'] = StringIO(soapRequestTxt)
112            return self._app(environ, start_response)
113       
[5637]114        log.debug("SOAPAttributeInterfaceMiddleware.__call__: received SAML "
115                  "SOAP AttributeQuery ...")
116       
[5623]117        attributeQueryElem = soapRequest.body.elem[0]
118        attributeQuery = AttributeQueryElementTree.fromXML(attributeQueryElem)
[5637]119               
[5623]120        samlResponse = Response()
121       
122        samlResponse.issueInstant = datetime.utcnow()
123        samlResponse.id = str(uuid4())
124        samlResponse.issuer = Issuer()
125       
126        # SAML 2.0 spec says fromat must be omitted
127        #samlResponse.issuer.format = Issuer.X509_SUBJECT
128        samlResponse.issuer.value = \
129                        "/O=NDG/OU=BADC/CN=attributeauthority.badc.rl.ac.uk"
130       
131        samlResponse.inResponseTo = attributeQuery.id
132       
133        assertion = Assertion()
134       
135        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
136        assertion.id = str(uuid4())
137        assertion.issueInstant = samlResponse.issueInstant
138       
139        assertion.conditions = Conditions()
140        assertion.conditions.notBefore = assertion.issueInstant
141        assertion.conditions.notOnOrAfter = assertion.conditions.notBefore + \
142            timedelta(seconds=60*60*8)
143       
144        assertion.subject = Subject() 
145        assertion.subject.nameID = NameID()
146        assertion.subject.nameID.format = attributeQuery.subject.nameID.format
147        assertion.subject.nameID.value = attributeQuery.subject.nameID.value
148
149        assertion.attributeStatements.append(AttributeStatement())
150       
151        for attribute in attributeQuery.attributes:
152            if attribute.name == "urn:esg:first:name":
153                # special case handling for 'FirstName' attribute
154                fnAttribute = Attribute()
155                fnAttribute.name = attribute.name
156                fnAttribute.nameFormat = attribute.nameFormat
157                fnAttribute.friendlyName = attribute.friendlyName
158   
159                firstName = XSStringAttributeValue()
160                firstName.value = self.firstName
161                fnAttribute.attributeValues.append(firstName)
162   
163                assertion.attributeStatements[0].attributes.append(fnAttribute)
164           
165            elif attribute.name == "urn:esg:last:name":
166                lnAttribute = Attribute()
167                lnAttribute.name = attribute.name
168                lnAttribute.nameFormat = attribute.nameFormat
169                lnAttribute.friendlyName = attribute.friendlyName
170   
171                lastName = XSStringAttributeValue()
172                lastName.value = self.lastName
173                lnAttribute.attributeValues.append(lastName)
174   
175                assertion.attributeStatements[0].attributes.append(lnAttribute)
176               
177            elif attribute.name == "urn:esg:email:address":
178                emailAddressAttribute = Attribute()
179                emailAddressAttribute.name = attribute.name
180                emailAddressAttribute.nameFormat = attribute.nameFormat
181                emailAddressAttribute.friendlyName = attribute.friendlyName
182   
183                emailAddress = XSStringAttributeValue()
184                emailAddress.value = self.emailAddress
185                emailAddressAttribute.attributeValues.append(emailAddress)
186   
187                assertion.attributeStatements[0].attributes.append(
188                                                        emailAddressAttribute)
189       
190        samlResponse.assertions.append(assertion)
191       
192        # Add mapping for ESG Group/Role Attribute Value to enable ElementTree
193        # Attribute Value factory to render the XML output
194        toXMLTypeMap = {
195            XSGroupRoleAttributeValue: XSGroupRoleAttributeValueElementTree
196        }
197
198       
199        samlResponse.status = Status()
200        samlResponse.status.statusCode = StatusCode()
201        samlResponse.status.statusCode.value = StatusCode.SUCCESS_URI       
202
203       
204        # Convert to ElementTree representation to enable attachment to SOAP
205        # response body
206        samlResponseElem = ResponseElementTree.toXML(samlResponse,
207                                            customToXMLTypeMap=toXMLTypeMap)
208        xml = ElementTree.tostring(samlResponseElem)
209       
210        # Create SOAP response and attach the SAML Response payload
211        soapResponse = SOAPEnvelope()
212        soapResponse.create()
213        soapResponse.body.elem.append(samlResponseElem)
214       
215        response = soapResponse.serialize()
216       
217        start_response("200 OK",
218                       [('Content-length', str(len(response))),
219                        ('Content-type', 'text/xml')])
220        return [response]
221   
222    @classmethod
223    def isAttributeQuery(cls, soapBody):
224        """Check for AttributeQuery in the SOAP Body"""
225       
226        if len(soapBody.elem) != 1:
227            # TODO: Change to a SOAP Fault?
228            raise SOAPAttributeInterfaceMiddlewareError("Expecting single "
229                                                        "child element in the "
230                                                        "request SOAP "
231                                                        "Envelope body")
232       
233        return QName(soapBody.elem.tag) == AttributeQuery.DEFAULT_ELEMENT_NAME
Note: See TracBrowser for help on using the repository browser.