source: TI12-security/trunk/ndg_saml/ndg/saml/test/binding/soap/test_queryresponseinterface.py @ 7526

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/ndg_saml/ndg/saml/test/binding/soap/test_queryresponseinterface.py@7526
Revision 7526, 19.3 KB checked in by pjkersha, 10 years ago (diff)

Preparing new release. All working tests pass:

  • fixed X.509 Subject name urn
  • added test for content length for query input to query interface
  • fixed test_queryresponse - removed ESG specific attribute references.
  • Property svn:keywords set to Id
Line 
1"""SAML Generic SOAP Binding Query/Response Interface unit test module
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "21/07/09"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__license__ = "http://www.apache.org/licenses/LICENSE-2.0"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = '$Id$'
11import logging
12logging.basicConfig(level=logging.DEBUG)
13log = logging.getLogger(__name__)
14import unittest
15
16from datetime import datetime, timedelta
17import os
18from uuid import uuid4
19import paste.fixture
20from cStringIO import StringIO
21from xml.etree import ElementTree
22
23from ndg.saml.utils import SAMLDateTime
24from ndg.saml.saml2.core import (Response, Assertion, Attribute, 
25                             AttributeStatement, SAMLVersion, Subject, NameID,
26                             Issuer, AttributeQuery, XSStringAttributeValue, 
27                             Conditions, Status, StatusCode)
28from ndg.saml.xml import XMLConstants
29from ndg.saml.xml.etree import AttributeQueryElementTree, ResponseElementTree
30from ndg.saml.saml2.binding.soap.client.subjectquery import (
31    SubjectQuerySOAPBinding, ResponseIssueInstantInvalid, 
32    AssertionIssueInstantInvalid, AssertionConditionNotBeforeInvalid, 
33    AssertionConditionNotOnOrAfterInvalid)
34
35from ndg.soap.client import (UrlLib2SOAPClient, UrlLib2SOAPRequest)
36from ndg.soap.etree import SOAPEnvelope
37from ndg.soap.utils.etree import QName, prettyPrint
38
39
40class SamlSoapBindingApp(object):
41    """Simple WSGI application to handle SAML Attribute Query/Response
42    """
43    FIRSTNAME_ATTRNAME = "urn:ndg:saml:firstname"
44    LASTNAME_ATTRNAME = "urn:ndg:saml:lastname"
45    EMAILADDRESS_ATTRNAME = "urn:ndg:saml:emailaddress"
46    NAMEID_FORMAT = "urn:ndg:saml:openid"
47   
48    def __init__(self):
49        self.firstName = "Philip"
50        self.lastName = "Kershaw"
51        self.emailAddress = "pkershaw@somewhere.ac.uk"
52                 
53    def __call__(self, environ, start_response):
54        soapRequestStream = environ['wsgi.input']
55        soapRequest = SOAPEnvelope()
56        soapRequest.parse(soapRequestStream)
57        attributeQueryElem = soapRequest.body.elem[0]
58        attributeQuery = AttributeQueryElementTree.fromXML(attributeQueryElem)
59       
60        print("Received request from client:\n")
61        print soapRequest.prettyPrint()
62       
63        samlResponse = Response()
64       
65        samlResponse.issueInstant = datetime.utcnow()
66        samlResponse.id = str(uuid4())
67        samlResponse.issuer = Issuer()
68       
69        # SAML 2.0 spec says format must be omitted
70        #samlResponse.issuer.format = Issuer.X509_SUBJECT
71        samlResponse.issuer.value = \
72                        "/O=NDG/OU=BADC/CN=attributeauthority.badc.rl.ac.uk"
73       
74        samlResponse.inResponseTo = attributeQuery.id
75       
76        assertion = Assertion()
77       
78        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
79        assertion.id = str(uuid4())
80        assertion.issueInstant = samlResponse.issueInstant
81       
82        assertion.conditions = Conditions()
83        assertion.conditions.notBefore = assertion.issueInstant
84        assertion.conditions.notOnOrAfter = assertion.conditions.notBefore + \
85            timedelta(seconds=60*60*8)
86       
87        assertion.subject = Subject() 
88        assertion.subject.nameID = NameID()
89        assertion.subject.nameID.format = attributeQuery.subject.nameID.format
90        assertion.subject.nameID.value = attributeQuery.subject.nameID.value
91
92        assertion.attributeStatements.append(AttributeStatement())
93       
94        for attribute in attributeQuery.attributes:
95            if attribute.name == SamlSoapBindingApp.FIRSTNAME_ATTRNAME:
96                # special case handling for 'FirstName' attribute
97                fnAttribute = Attribute()
98                fnAttribute.name = attribute.name
99                fnAttribute.nameFormat = attribute.nameFormat
100                fnAttribute.friendlyName = attribute.friendlyName
101   
102                firstName = XSStringAttributeValue()
103                firstName.value = self.firstName
104                fnAttribute.attributeValues.append(firstName)
105   
106                assertion.attributeStatements[0].attributes.append(fnAttribute)
107           
108            elif attribute.name == SamlSoapBindingApp.LASTNAME_ATTRNAME:
109                lnAttribute = Attribute()
110                lnAttribute.name = attribute.name
111                lnAttribute.nameFormat = attribute.nameFormat
112                lnAttribute.friendlyName = attribute.friendlyName
113   
114                lastName = XSStringAttributeValue()
115                lastName.value = self.lastName
116                lnAttribute.attributeValues.append(lastName)
117   
118                assertion.attributeStatements[0].attributes.append(lnAttribute)
119               
120            elif attribute.name == SamlSoapBindingApp.EMAILADDRESS_ATTRNAME:
121                emailAddressAttribute = Attribute()
122                emailAddressAttribute.name = attribute.name
123                emailAddressAttribute.nameFormat = attribute.nameFormat
124                emailAddressAttribute.friendlyName = attribute.friendlyName
125   
126                emailAddress = XSStringAttributeValue()
127                emailAddress.value = self.emailAddress
128                emailAddressAttribute.attributeValues.append(emailAddress)
129   
130                assertion.attributeStatements[0].attributes.append(
131                                                        emailAddressAttribute)
132       
133        samlResponse.assertions.append(assertion)
134       
135        samlResponse.status = Status()
136        samlResponse.status.statusCode = StatusCode()
137        samlResponse.status.statusCode.value = StatusCode.SUCCESS_URI       
138
139       
140        # Convert to ElementTree representation to enable attachment to SOAP
141        # response body
142        samlResponseElem = ResponseElementTree.toXML(samlResponse)
143        xml = ElementTree.tostring(samlResponseElem)
144        log.debug('Sending response to query:\n%s', xml)
145       
146        # Create SOAP response and attach the SAML Response payload
147        soapResponse = SOAPEnvelope()
148        soapResponse.create()
149        soapResponse.body.elem.append(samlResponseElem)
150       
151        response = soapResponse.serialize()
152       
153        start_response("200 OK",
154                       [('Content-length', str(len(response))),
155                        ('Content-type', 'text/xml')])
156        return [response]
157
158       
159class SamlAttributeQueryTestCase(unittest.TestCase):
160    """Test the SAML SOAP binding using an Attribute Query as an example"""
161    thisDir = os.path.dirname(os.path.abspath(__file__))
162    RESPONSE = '''\
163<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
164   <soap11:Body>
165      <samlp:Response ID="05680cb2-4973-443d-9d31-7bc99bea87c1" InResponseTo="e3183380-ae82-4285-8827-8c40613842de" IssueInstant="%(issueInstant)s" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
166         <saml:Issuer Format="urn:esg:issuer" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">Somewhere</saml:Issuer>
167         <samlp:Status>
168            <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
169         </samlp:Status>
170         <saml:Assertion ID="192c67d9-f9cd-457a-9242-999e7b943166" IssueInstant="%(assertionIssueInstant)s" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
171            <saml:Issuer Format="urn:esg:issuer">Somewhere</saml:Issuer>
172            <saml:Subject>
173               <saml:NameID Format="urn:esg:openid">https://somewhere.edu/myopenid/testUser</saml:NameID>
174            </saml:Subject>
175            <saml:Conditions NotBefore="%(notBefore)s" NotOnOrAfter="%(notOnOrAfter)s" />
176            <saml:AttributeStatement>
177               <saml:Attribute FriendlyName="FirstName" Name="urn:esg:first:name" NameFormat="http://www.w3.org/2001/XMLSchema#string">
178                  <saml:AttributeValue xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Test</saml:AttributeValue>
179               </saml:Attribute>
180               <saml:Attribute FriendlyName="LastName" Name="urn:esg:last:name" NameFormat="http://www.w3.org/2001/XMLSchema#string">
181                  <saml:AttributeValue xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">User</saml:AttributeValue>
182               </saml:Attribute>
183               <saml:Attribute FriendlyName="EmailAddress" Name="urn:esg:first:email:address" NameFormat="http://www.w3.org/2001/XMLSchema#string">
184                  <saml:AttributeValue xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">someone@somewhere.edu</saml:AttributeValue>
185               </saml:Attribute>
186            </saml:AttributeStatement>
187         </saml:Assertion>
188      </samlp:Response>
189   </soap11:Body>
190</soap11:Envelope>
191'''
192
193    def __init__(self, *args, **kwargs):
194        wsgiApp = SamlSoapBindingApp()
195        self.app = paste.fixture.TestApp(wsgiApp)
196         
197        unittest.TestCase.__init__(self, *args, **kwargs)
198       
199    def test01AttributeQuery(self):
200        attributeQuery = AttributeQuery()
201        attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
202        attributeQuery.id = str(uuid4())
203        attributeQuery.issueInstant = datetime.utcnow()
204       
205        attributeQuery.issuer = Issuer()
206        attributeQuery.issuer.format = Issuer.X509_SUBJECT
207        attributeQuery.issuer.value = \
208                        "/O=NDG/OU=BADC/CN=attributeauthority.badc.rl.ac.uk"
209                       
210                       
211        attributeQuery.subject = Subject() 
212        attributeQuery.subject.nameID = NameID()
213        attributeQuery.subject.nameID.format = SamlSoapBindingApp.NAMEID_FORMAT
214        attributeQuery.subject.nameID.value = \
215                                    "https://openid.localhost/philip.kershaw"
216       
217        # special case handling for 'FirstName' attribute
218        fnAttribute = Attribute()
219        fnAttribute.name = SamlSoapBindingApp.FIRSTNAME_ATTRNAME
220        fnAttribute.nameFormat = "http://www.w3.org/2001/XMLSchema#string"
221        fnAttribute.friendlyName = "FirstName"
222
223        attributeQuery.attributes.append(fnAttribute)
224   
225        # special case handling for 'LastName' attribute
226        lnAttribute = Attribute()
227        lnAttribute.name = SamlSoapBindingApp.LASTNAME_ATTRNAME
228        lnAttribute.nameFormat = "http://www.w3.org/2001/XMLSchema#string"
229        lnAttribute.friendlyName = "LastName"
230
231        attributeQuery.attributes.append(lnAttribute)
232   
233        # special case handling for 'LastName' attribute
234        emailAddressAttribute = Attribute()
235        emailAddressAttribute.name = SamlSoapBindingApp.EMAILADDRESS_ATTRNAME
236        emailAddressAttribute.nameFormat = XMLConstants.XSD_NS+"#"+\
237                                    XSStringAttributeValue.TYPE_LOCAL_NAME
238        emailAddressAttribute.friendlyName = "emailAddress"
239
240        attributeQuery.attributes.append(emailAddressAttribute)                                   
241       
242        elem = AttributeQueryElementTree.toXML(attributeQuery)
243        soapRequest = SOAPEnvelope()
244        soapRequest.create()
245        soapRequest.body.elem.append(elem)
246       
247        request = soapRequest.serialize()
248       
249        header = {
250            'soapAction': "http://www.oasis-open.org/committees/security",
251            'Content-length': str(len(request)),
252            'Content-type': 'text/xml'
253        }
254        response = self.app.post('/attributeauthority', 
255                                 params=request, 
256                                 headers=header, 
257                                 status=200)
258        print("Response status=%d" % response.status)
259
260        soapResponse = SOAPEnvelope()
261       
262        responseStream = StringIO()
263        responseStream.write(response.body)
264        responseStream.seek(0)
265       
266        soapResponse.parse(responseStream)
267       
268        print("Parsed response ...")
269        print(soapResponse.serialize())
270#        print(prettyPrint(soapResponse.elem))
271       
272        response = ResponseElementTree.fromXML(soapResponse.body.elem[0])
273        self.assert_(response.status.statusCode.value==StatusCode.SUCCESS_URI)
274        self.assert_(response.inResponseTo == attributeQuery.id)
275        self.assert_(response.assertions[0].subject.nameID.value == \
276                     attributeQuery.subject.nameID.value)
277
278    def _parseResponse(self, responseStr):
279        """Helper to parse a response from a string"""
280        soapResponse = SOAPEnvelope()
281       
282        responseStream = StringIO()
283        responseStream.write(responseStr)
284        responseStream.seek(0)
285       
286        soapResponse.parse(responseStream)
287       
288        print("Parsed response ...")
289        print(soapResponse.serialize())
290       
291        response = ResponseElementTree.fromXML(soapResponse.body.elem[0])
292        return response
293       
294    def test03ParseResponse(self):
295        utcNow = datetime.utcnow()
296        respDict = {
297            'issueInstant': SAMLDateTime.toString(utcNow),
298            'assertionIssueInstant': SAMLDateTime.toString(utcNow),
299            'notBefore': SAMLDateTime.toString(utcNow),
300            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
301                                                            seconds=60*60*8))
302        }
303        responseStr = self.__class__.RESPONSE % \
304                                                                        respDict
305        response = self._parseResponse(responseStr)
306        self.assert_(response)
307
308    def test04AssertionConditionExpired(self):
309        # issued 9 hours ago
310        issueInstant = datetime.utcnow() - timedelta(seconds=60*60*9)
311        respDict = {
312            'issueInstant': SAMLDateTime.toString(issueInstant),
313            'assertionIssueInstant': SAMLDateTime.toString(issueInstant),
314            'notBefore': SAMLDateTime.toString(issueInstant),
315            # It lasts for 8 hours so it's expired by one hour
316            'notOnOrAfter': SAMLDateTime.toString(issueInstant + timedelta(
317                                                            seconds=60*60*8))
318        }
319        responseStr = self.__class__.RESPONSE % \
320                                                                    respDict
321        response = self._parseResponse(responseStr)
322        binding = SubjectQuerySOAPBinding()
323        try:
324            binding._verifyTimeConditions(response)
325            self.fail("Expecting not on or after timestamp error")
326        except AssertionConditionNotOnOrAfterInvalid, e:
327            print("PASSED: %s" % e)
328
329    def test05ResponseIssueInstantInvalid(self):
330        utcNow = datetime.utcnow()
331        respDict = {
332            'issueInstant': SAMLDateTime.toString(utcNow + timedelta(
333                                                                    seconds=1)),
334            'assertionIssueInstant': SAMLDateTime.toString(utcNow),
335            'notBefore': SAMLDateTime.toString(utcNow),
336            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
337                                                            seconds=60*60*8))
338        }
339        responseStr = self.__class__.RESPONSE % \
340                                                                    respDict
341        response = self._parseResponse(responseStr)
342        binding = SubjectQuerySOAPBinding()
343        try:
344            binding._verifyTimeConditions(response)
345            self.fail("Expecting issue instant timestamp error")
346        except ResponseIssueInstantInvalid, e:
347            print("PASSED: %s" % e)
348
349    def test06NotBeforeConditionInvalid(self):
350        utcNow = datetime.utcnow()
351        respDict = {
352            'issueInstant': SAMLDateTime.toString(utcNow),
353            'assertionIssueInstant': SAMLDateTime.toString(utcNow),
354            'notBefore': SAMLDateTime.toString(utcNow + timedelta(seconds=1)),
355            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
356                                                            seconds=60*60*8))
357        }
358        responseStr = self.__class__.RESPONSE % \
359                                                                    respDict
360        response = self._parseResponse(responseStr)
361        binding = SubjectQuerySOAPBinding()
362        try:
363            binding._verifyTimeConditions(response)
364            self.fail("Expecting issue instant timestamp error")
365        except AssertionConditionNotBeforeInvalid, e:
366            print("PASSED: %s" % e)
367
368    def test07AssertionIssueInstantInvalid(self):
369        utcNow = datetime.utcnow()
370        respDict = {
371            'issueInstant': SAMLDateTime.toString(utcNow),
372            'assertionIssueInstant': SAMLDateTime.toString(utcNow + timedelta(
373                                                                    seconds=1)),
374            'notBefore': SAMLDateTime.toString(utcNow),
375            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
376                                                            seconds=60*60*8))
377        }
378        responseStr = self.__class__.RESPONSE % \
379                                                                    respDict
380        response = self._parseResponse(responseStr)
381        binding = SubjectQuerySOAPBinding()
382        try:
383            binding._verifyTimeConditions(response)
384            self.fail("Expecting issue instant timestamp error")
385        except AssertionIssueInstantInvalid, e:
386            print("PASSED: %s" % e)
387
388    def test07ClockSkewCorrectedAssertionIssueInstantInvalid(self):
389        utcNow = datetime.utcnow()
390        respDict = {
391            'issueInstant': SAMLDateTime.toString(utcNow),
392            'assertionIssueInstant': SAMLDateTime.toString(utcNow + timedelta(
393                                                                    seconds=1)),
394            'notBefore': SAMLDateTime.toString(utcNow),
395            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
396                                                            seconds=60*60*8))
397        }
398        responseStr = self.__class__.RESPONSE % \
399                                                                    respDict
400        response = self._parseResponse(responseStr)
401        binding = SubjectQuerySOAPBinding()
402       
403        # Set a skew to correct the error
404        binding.clockSkewTolerance = 1
405       
406        try:
407            binding._verifyTimeConditions(response)
408        except AssertionIssueInstantInvalid, e:
409            self.fail("issue instant timestamp error should be corrected for")
410
411    def test08ClockSkewCorrectedAssertionConditionExpired(self):
412        # Issued 9 hours ago
413        issueInstant = datetime.utcnow() - timedelta(seconds=60*60*9)
414        respDict = {
415            'issueInstant': SAMLDateTime.toString(issueInstant),
416            'assertionIssueInstant': SAMLDateTime.toString(issueInstant),
417            'notBefore': SAMLDateTime.toString(issueInstant),
418            # Assertion lasts 8 hours so it has expired by one hour
419            'notOnOrAfter': SAMLDateTime.toString(issueInstant + timedelta(
420                                                            seconds=60*60*8))
421        }
422        responseStr = self.__class__.RESPONSE % respDict
423        response = self._parseResponse(responseStr)
424        binding = SubjectQuerySOAPBinding()
425       
426        # Set a skew of over one hour to correct for the assertion expiry
427        binding.clockSkewTolerance = 60*60 + 3
428       
429        try:
430            binding._verifyTimeConditions(response)
431           
432        except AssertionConditionNotOnOrAfterInvalid, e:
433            self.fail("Not on or after timestamp error should be corrected for")
434           
435           
436if __name__ == "__main__":
437    unittest.main()       
438
Note: See TracBrowser for help on using the repository browser.