Package ndg :: Package saml :: Package test :: Package binding :: Package soap :: Module test_queryresponseinterface
[hide private]

Source Code for Module ndg.saml.test.binding.soap.test_queryresponseinterface

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