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.

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.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       
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
103        soapRequest = SOAPEnvelope()
104        soapRequest.parse(StringIO(soapRequestTxt))
105       
106        # Filter based on SOAP Body content - expecting an AttributeQuery
107        # element
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       
114        log.debug("SOAPAttributeInterfaceMiddleware.__call__: received SAML "
115                  "SOAP AttributeQuery ...")
116       
117        attributeQueryElem = soapRequest.body.elem[0]
118        attributeQuery = AttributeQueryElementTree.fromXML(attributeQueryElem)
119               
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.