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

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

Incomplete - task 2: XACML-Security Integration

  • added test_attributeservice_paster - tests an attribute service over an SSL connection
  • 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                                            customToXMLTypeMap=toXMLTypeMap)
144        xml = ElementTree.tostring(samlResponseElem)
145        log.debug('Sending response to query:\n%s', xml)
146       
147        # Create SOAP response and attach the SAML Response payload
148        soapResponse = SOAPEnvelope()
149        soapResponse.create()
150        soapResponse.body.elem.append(samlResponseElem)
151       
152        response = soapResponse.serialize()
153       
154        start_response("200 OK",
155                       [('Content-length', str(len(response))),
156                        ('Content-type', 'text/xml')])
157        return [response]
158
159       
160class SamlAttributeQueryTestCase(unittest.TestCase):
161    """Test the SAML SOAP binding using an Attribute Query as an example"""
162    thisDir = os.path.dirname(os.path.abspath(__file__))
163    RESPONSE = '''\
164<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
165   <SOAP-ENV:Body>
166      <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">
167         <saml:Issuer Format="urn:esg:issuer" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">ESG-NCAR</saml:Issuer>
168         <samlp:Status>
169            <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
170         </samlp:Status>
171         <saml:Assertion ID="192c67d9-f9cd-457a-9242-999e7b943166" IssueInstant="%(assertionIssueInstant)s" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
172            <saml:Issuer Format="urn:esg:issuer">ESG-NCAR</saml:Issuer>
173            <saml:Subject>
174               <saml:NameID Format="urn:esg:openid">https://esg.prototype.ucar.edu/myopenid/testUser</saml:NameID>
175            </saml:Subject>
176            <saml:Conditions NotBefore="%(notBefore)s" NotOnOrAfter="%(notOnOrAfter)s" />
177            <saml:AttributeStatement>
178               <saml:Attribute FriendlyName="FirstName" Name="urn:esg:first:name" NameFormat="http://www.w3.org/2001/XMLSchema#string">
179                  <saml:AttributeValue xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Test</saml:AttributeValue>
180               </saml:Attribute>
181               <saml:Attribute FriendlyName="LastName" Name="urn:esg:last:name" NameFormat="http://www.w3.org/2001/XMLSchema#string">
182                  <saml:AttributeValue xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">User</saml:AttributeValue>
183               </saml:Attribute>
184               <saml:Attribute FriendlyName="EmailAddress" Name="urn:esg:first:email:address" NameFormat="http://www.w3.org/2001/XMLSchema#string">
185                  <saml:AttributeValue xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">ejn@ucar.edu</saml:AttributeValue>
186               </saml:Attribute>
187               <saml:Attribute FriendlyName="GroupRole" Name="urn:esg:group:role" NameFormat="groupRole">
188                  <saml:AttributeValue>
189                     <esg:groupRole group="CCSM" role="default" xmlns:esg="http://www.esg.org" />
190                  </saml:AttributeValue>
191                  <saml:AttributeValue>
192                     <esg:groupRole group="Dynamical Core" role="default" xmlns:esg="http://www.esg.org" />
193                  </saml:AttributeValue>
194                  <saml:AttributeValue>
195                     <esg:groupRole group="NARCCAP" role="default" xmlns:esg="http://www.esg.org" />
196                  </saml:AttributeValue>
197               </saml:Attribute>
198            </saml:AttributeStatement>
199         </saml:Assertion>
200      </samlp:Response>
201   </SOAP-ENV:Body>
202</SOAP-ENV:Envelope>
203'''
204
205    def __init__(self, *args, **kwargs):
206        wsgiApp = SamlSoapBindingApp()
207        self.app = paste.fixture.TestApp(wsgiApp)
208         
209        unittest.TestCase.__init__(self, *args, **kwargs)
210       
211    def test01AttributeQuery(self):
212        attributeQuery = AttributeQuery()
213        attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
214        attributeQuery.id = str(uuid4())
215        attributeQuery.issueInstant = datetime.utcnow()
216       
217        attributeQuery.issuer = Issuer()
218        attributeQuery.issuer.format = Issuer.X509_SUBJECT
219        attributeQuery.issuer.value = \
220                        "/O=NDG/OU=BADC/CN=attributeauthority.badc.rl.ac.uk"
221                       
222                       
223        attributeQuery.subject = Subject() 
224        attributeQuery.subject.nameID = NameID()
225        attributeQuery.subject.nameID.format = SamlSoapBindingApp.NAMEID_FORMAT
226        attributeQuery.subject.nameID.value = \
227                                    "https://openid.localhost/philip.kershaw"
228       
229        # special case handling for 'FirstName' attribute
230        fnAttribute = Attribute()
231        fnAttribute.name = SamlSoapBindingApp.FIRSTNAME_ATTRNAME
232        fnAttribute.nameFormat = "http://www.w3.org/2001/XMLSchema#string"
233        fnAttribute.friendlyName = "FirstName"
234
235        attributeQuery.attributes.append(fnAttribute)
236   
237        # special case handling for 'LastName' attribute
238        lnAttribute = Attribute()
239        lnAttribute.name = SamlSoapBindingApp.LASTNAME_ATTRNAME
240        lnAttribute.nameFormat = "http://www.w3.org/2001/XMLSchema#string"
241        lnAttribute.friendlyName = "LastName"
242
243        attributeQuery.attributes.append(lnAttribute)
244   
245        # special case handling for 'LastName' attribute
246        emailAddressAttribute = Attribute()
247        emailAddressAttribute.name = SamlSoapBindingApp.EMAILADDRESS_ATTRNAME
248        emailAddressAttribute.nameFormat = XMLConstants.XSD_NS+"#"+\
249                                    XSStringAttributeValue.TYPE_LOCAL_NAME
250        emailAddressAttribute.friendlyName = "emailAddress"
251
252        attributeQuery.attributes.append(emailAddressAttribute)                                   
253       
254        elem = AttributeQueryElementTree.toXML(attributeQuery)
255        soapRequest = SOAPEnvelope()
256        soapRequest.create()
257        soapRequest.body.elem.append(elem)
258       
259        request = soapRequest.serialize()
260       
261        header = {
262            'soapAction': "http://www.oasis-open.org/committees/security",
263            'Content-length': str(len(request)),
264            'Content-type': 'text/xml'
265        }
266        response = self.app.post('/attributeauthority', 
267                                 params=request, 
268                                 headers=header, 
269                                 status=200)
270        print("Response status=%d" % response.status)
271
272        soapResponse = SOAPEnvelope()
273       
274        responseStream = StringIO()
275        responseStream.write(response.body)
276        responseStream.seek(0)
277       
278        soapResponse.parse(responseStream)
279       
280        print("Parsed response ...")
281        print(soapResponse.serialize())
282#        print(prettyPrint(soapResponse.elem))
283       
284        response = ResponseElementTree.fromXML(soapResponse.body.elem[0])
285        self.assert_(response.status.statusCode.value==StatusCode.SUCCESS_URI)
286        self.assert_(response.inResponseTo == attributeQuery.id)
287        self.assert_(response.assertions[0].subject.nameID.value == \
288                     attributeQuery.subject.nameID.value)
289
290    def _parseResponse(self, responseStr):
291        """Helper to parse a response from a string"""
292        soapResponse = SOAPEnvelope()
293       
294        responseStream = StringIO()
295        responseStream.write(responseStr)
296        responseStream.seek(0)
297       
298        soapResponse.parse(responseStream)
299       
300        print("Parsed response ...")
301        print(soapResponse.serialize())
302       
303        response = ResponseElementTree.fromXML(soapResponse.body.elem[0])
304        return response
305       
306    def test03ParseResponse(self):
307        utcNow = datetime.utcnow()
308        respDict = {
309            'issueInstant': SAMLDateTime.toString(utcNow),
310            'assertionIssueInstant': SAMLDateTime.toString(utcNow),
311            'notBefore': SAMLDateTime.toString(utcNow),
312            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
313                                                            seconds=60*60*8))
314        }
315        responseStr = self.__class__.RESPONSE % \
316                                                                        respDict
317        response = self._parseResponse(responseStr)
318        self.assert_(response)
319
320    def test04AssertionConditionExpired(self):
321        # issued 9 hours ago
322        issueInstant = datetime.utcnow() - timedelta(seconds=60*60*9)
323        respDict = {
324            'issueInstant': SAMLDateTime.toString(issueInstant),
325            'assertionIssueInstant': SAMLDateTime.toString(issueInstant),
326            'notBefore': SAMLDateTime.toString(issueInstant),
327            # It lasts for 8 hours so it's expired by one hour
328            'notOnOrAfter': SAMLDateTime.toString(issueInstant + timedelta(
329                                                            seconds=60*60*8))
330        }
331        responseStr = self.__class__.RESPONSE % \
332                                                                    respDict
333        response = self._parseResponse(responseStr)
334        binding = SubjectQuerySOAPBinding()
335        try:
336            binding._verifyTimeConditions(response)
337            self.fail("Expecting not on or after timestamp error")
338        except AssertionConditionNotOnOrAfterInvalid, e:
339            print("PASSED: %s" % e)
340
341    def test05ResponseIssueInstantInvalid(self):
342        utcNow = datetime.utcnow()
343        respDict = {
344            'issueInstant': SAMLDateTime.toString(utcNow + timedelta(
345                                                                    seconds=1)),
346            'assertionIssueInstant': SAMLDateTime.toString(utcNow),
347            'notBefore': SAMLDateTime.toString(utcNow),
348            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
349                                                            seconds=60*60*8))
350        }
351        responseStr = self.__class__.RESPONSE % \
352                                                                    respDict
353        response = self._parseResponse(responseStr)
354        binding = SubjectQuerySOAPBinding()
355        try:
356            binding._verifyTimeConditions(response)
357            self.fail("Expecting issue instant timestamp error")
358        except ResponseIssueInstantInvalid, e:
359            print("PASSED: %s" % e)
360
361    def test06NotBeforeConditionInvalid(self):
362        utcNow = datetime.utcnow()
363        respDict = {
364            'issueInstant': SAMLDateTime.toString(utcNow),
365            'assertionIssueInstant': SAMLDateTime.toString(utcNow),
366            'notBefore': SAMLDateTime.toString(utcNow + timedelta(seconds=1)),
367            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
368                                                            seconds=60*60*8))
369        }
370        responseStr = self.__class__.RESPONSE % \
371                                                                    respDict
372        response = self._parseResponse(responseStr)
373        binding = SubjectQuerySOAPBinding()
374        try:
375            binding._verifyTimeConditions(response)
376            self.fail("Expecting issue instant timestamp error")
377        except AssertionConditionNotBeforeInvalid, e:
378            print("PASSED: %s" % e)
379
380    def test07AssertionIssueInstantInvalid(self):
381        utcNow = datetime.utcnow()
382        respDict = {
383            'issueInstant': SAMLDateTime.toString(utcNow),
384            'assertionIssueInstant': SAMLDateTime.toString(utcNow + timedelta(
385                                                                    seconds=1)),
386            'notBefore': SAMLDateTime.toString(utcNow),
387            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
388                                                            seconds=60*60*8))
389        }
390        responseStr = self.__class__.RESPONSE % \
391                                                                    respDict
392        response = self._parseResponse(responseStr)
393        binding = SubjectQuerySOAPBinding()
394        try:
395            binding._verifyTimeConditions(response)
396            self.fail("Expecting issue instant timestamp error")
397        except AssertionIssueInstantInvalid, e:
398            print("PASSED: %s" % e)
399
400    def test07ClockSkewCorrectedAssertionIssueInstantInvalid(self):
401        utcNow = datetime.utcnow()
402        respDict = {
403            'issueInstant': SAMLDateTime.toString(utcNow),
404            'assertionIssueInstant': SAMLDateTime.toString(utcNow + timedelta(
405                                                                    seconds=1)),
406            'notBefore': SAMLDateTime.toString(utcNow),
407            'notOnOrAfter': SAMLDateTime.toString(utcNow + timedelta(
408                                                            seconds=60*60*8))
409        }
410        responseStr = self.__class__.RESPONSE % \
411                                                                    respDict
412        response = self._parseResponse(responseStr)
413        binding = SubjectQuerySOAPBinding()
414       
415        # Set a skew to correct the error
416        binding.clockSkewTolerance = 1
417       
418        try:
419            binding._verifyTimeConditions(response)
420        except AssertionIssueInstantInvalid, e:
421            self.fail("issue instant timestamp error should be corrected for")
422
423    def test08ClockSkewCorrectedAssertionConditionExpired(self):
424        # Issued 9 hours ago
425        issueInstant = datetime.utcnow() - timedelta(seconds=60*60*9)
426        respDict = {
427            'issueInstant': SAMLDateTime.toString(issueInstant),
428            'assertionIssueInstant': SAMLDateTime.toString(issueInstant),
429            'notBefore': SAMLDateTime.toString(issueInstant),
430            # Assertion lasts 8 hours so it has expired by one hour
431            'notOnOrAfter': SAMLDateTime.toString(issueInstant + timedelta(
432                                                            seconds=60*60*8))
433        }
434        responseStr = self.__class__.RESPONSE % respDict
435        response = self._parseResponse(responseStr)
436        binding = SubjectQuerySOAPBinding()
437       
438        # Set a skew of over one hour to correct for the assertion expiry
439        binding.clockSkewTolerance = 60*60 + 3
440       
441        try:
442            binding._verifyTimeConditions(response)
443           
444        except AssertionConditionNotOnOrAfterInvalid, e:
445            self.fail("Not on or after timestamp error should be corrected for")
446           
447           
448if __name__ == "__main__":
449    unittest.main()       
450
Note: See TracBrowser for help on using the repository browser.